dsh-rule-engine 0.6.3 → 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.
@@ -1,401 +0,0 @@
1
- // release-plugin.mjs —— DSH 插件全渠道一键发布(npm + git + GitHub Release)
2
- // 用法:node release-plugin.mjs <插件名|插件目录> [版本号] [--sync-profile]
3
- // - 插件名从 scripts/plugins.json 清单解析(规则 26 ⑤);或直接给目录路径
4
- // - 版本号省略时自动 patch+1(如 1.4.7 -> 1.4.8)
5
- // - 自动同步 README 徽章 version-X.Y.Z
6
- // - --sync-profile:发布成功后自动更新 profiles/<profile>/pnpm-workspace.yaml 豁免名单
7
- // (备份原文件)+ 运行全量装配审计(写 .dsh 需以 danger-full-access 运行本脚本)
8
- // 前置:npm 已认证、gh 已认证、git 已配置。
9
- // 网络:2026-09-03 实测修正——直连 github.com/npm registry 已不可靠(443 超时/连接重置);
10
- // 需代理环境请设 DSH_RELEASE_PROXY=http://127.0.0.1:7890(本机 Clash 7890)。未设时脚本仍尝试直连。
11
- // 步骤:版本 bump -> 测试 -> npm pack -> npm publish -> [publish 后 dist-tags 校验(B1,2026-09-03)]
12
- // -> git commit+push -> gh release(带 tgz asset)
13
- // 安全:token 经环境变量注入,不在命令文本/日志中打印
14
- import { execSync } from "node:child_process";
15
- import { existsSync, readFileSync, writeFileSync, mkdirSync, copyFileSync, statSync, rmSync } from "node:fs";
16
- import { dirname, join, relative, resolve } from "node:path";
17
- import { fileURLToPath } from "node:url";
18
- import { ensureExempt } from "./lib/pnpm-exempt.mjs";
19
-
20
- const PROXY = process.env.DSH_RELEASE_PROXY || ""; // 默认直连;需要代理时显式设置
21
- /** 代理前缀(命令文本):空 = 直连 */
22
- const proxyPrefix = () => (PROXY ? `set HTTPS_PROXY=${PROXY}&& set HTTP_PROXY=${PROXY}&& ` : "");
23
- const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url));
24
- const WORKSPACE = join(SCRIPT_DIR, "..");
25
-
26
- function run(cmd, opts = {}) {
27
- console.log(`> ${cmd.slice(0, 120)}${cmd.length > 120 ? "..." : ""}`);
28
- return execSync(cmd, { stdio: "inherit", encoding: "utf8", shell: true, ...opts });
29
- }
30
- function quiet(cmd) {
31
- try {
32
- return execSync(cmd, { encoding: "utf8", shell: true, stdio: "pipe" }).toString().trim();
33
- } catch {
34
- return "";
35
- }
36
- }
37
- function fail(msg) {
38
- console.error(`\n[发布中止] ${msg}`);
39
- process.exit(1);
40
- }
41
-
42
- // ── 0. 解析目标(清单名 或 目录路径)────────────────────────────
43
- const args = process.argv.slice(2);
44
- const syncProfile = args.includes("--sync-profile");
45
- const dryRun = args.includes("--dry-run"); // 2026-08-29:发布前预览(只推导+打印,不改文件)
46
- const target = args.find((a) => !a.startsWith("--")) || "";
47
- let manifest = null;
48
- let entry = null;
49
- let dir = null;
50
- try {
51
- manifest = JSON.parse(readFileSync(join(SCRIPT_DIR, "plugins.json"), "utf8"));
52
- entry = manifest.plugins.find((p) => p.name === target) || null;
53
- } catch {
54
- // 清单缺失时仅支持目录路径
55
- }
56
- if (entry) {
57
- dir = resolve(WORKSPACE, entry.dir);
58
- } else if (target) {
59
- dir = resolve(target);
60
- } else {
61
- fail("用法:node release-plugin.mjs <插件名|插件目录> [版本号] [--sync-profile]");
62
- }
63
- if (!existsSync(join(dir, "package.json"))) fail(`目录不存在或不是插件包:${dir}`);
64
-
65
- // ── 0. 读取包信息 ────────────────────────────────────────────────
66
- const pkgPath = join(dir, "package.json");
67
- const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
68
- const name = pkg.name;
69
- const oldVer = pkg.version;
70
- // 版本号 = 非 -- 开头的参数(排除目标名本身),位置任意
71
- // 2026-08-29 修复(0.5.11-pre 体系):oldVer 为 -pre/-rc 等预发布后缀时——
72
- // ① 发布语义 = 剥后缀发正式版(0.5.11-pre → 发布 0.5.11,不是 +1);
73
- // ② 自动推导不再 split(".") 直接 Number("11-pre" 会得 NaN——旧 bug)。
74
- // 显式传参优先;且显式传参如果是 -pre 形态(意外),本发布不剥(警告留痕)。
75
- const nextVer = args.find((a) => !a.startsWith("--") && a !== target) || (() => {
76
- // 预发布后缀(-pre/-rc)→ 剥后缀发正式版(0.5.11-pre → 0.5.11)
77
- if (/-(?:pre|rc|beta|alpha)(?:[.\d]*)$/i.test(oldVer)) return oldVer.replace(/-.*$/, "");
78
- // 纯数字 → +1(历史行为)
79
- const [maj, min, pat] = oldVer.split(".").map(Number);
80
- if (Number.isNaN(pat)) fail(`无法解析版本号:${oldVer}`);
81
- return `${maj}.${min}.${pat + 1}`;
82
- })();
83
- const repoOverride = entry?.repo || null;
84
- console.log(`\n=== 发布 ${name}: ${oldVer} -> ${nextVer} ===\n`);
85
-
86
- // ── 0.5 本地配套包版本一致性(防止 host/client 版本错位)──────────
87
- function checkDependencyPair() {
88
- if (!entry?.dependsOn || !manifest) return;
89
- for (const depName of entry.dependsOn) {
90
- const depEntry = manifest.plugins.find((p) => p.name === depName);
91
- const depDir = depEntry ? resolve(WORKSPACE, depEntry.dir) : null;
92
- if (!depDir || !existsSync(join(depDir, "package.json"))) {
93
- console.log(`(未找到依赖 ${depName} 的本地包,跳过版本一致性检查)`);
94
- continue;
95
- }
96
- const depPkg = JSON.parse(readFileSync(join(depDir, "package.json"), "utf8"));
97
- const range = pkg.dependencies?.[depName];
98
- if (!range) continue;
99
- const m = range.match(/^\^(\d+)\.(\d+)\.(\d+)/);
100
- if (!m) {
101
- console.log(`(依赖 ${depName} 范围 ${range} 不是简单 ^x.y.z,跳过自动校验)`);
102
- continue;
103
- }
104
- const v = depPkg.version.split(".").map(Number);
105
- const ok = v[0] === +m[1] && (v[1] > +m[2] || (v[1] === +m[2] && v[2] >= +m[3]));
106
- if (!ok) fail(`${name} 声明依赖 ${depName}@${range},但本地 ${depName} 是 ${depPkg.version}`);
107
- console.log(`[依赖一致性] ${name} -> ${depName} ${depPkg.version} ✓`);
108
- }
109
- }
110
- checkDependencyPair();
111
-
112
- // ── 1. 前置检查 ──────────────────────────────────────────────────
113
- if (!/^\d+\.\d+\.\d+$/.test(nextVer)) fail(`版本号格式错误:${nextVer}`);
114
- // 2026-09-02 修复:gh auth status 的 "Logged in" 输出在 stderr(新版 gh),stdout 可能为空空
115
- // → 2>&1 合并 stderr,避免已认证被误判"gh 未认证"(实测 keyring 已登录却判失败)
116
- if (!quiet("gh auth status 2>&1").includes("Logged in")) fail("gh 未认证");
117
- const whoami = quiet("npm whoami 2>&1");
118
- if (!whoami) fail("npm 未认证(npm whoami 失败)");
119
-
120
- // ── 1.5 DSH 兼容预检(发布前防“升级后插件不兼容”)──────────────
121
- const compatScript = join(SCRIPT_DIR, "check-dsh-compat.mjs");
122
- if (existsSync(compatScript)) {
123
- console.log("\n=== DSH 兼容预检 ===");
124
- run(`node "${compatScript}" "${dir}"`);
125
- }
126
-
127
- // ── 1.6 profile dump-config 冒烟(确认 DSH 可导出当前 profile 配置)──
128
- function findDshBin() {
129
- const candidates = [process.env.DSH_BIN, "D:/example/global-npm/dsh.cmd", "D:/example/global-npm/dsh", "dsh"].filter(Boolean);
130
- for (const c of candidates) {
131
- if (quiet(`where ${c}`)) return c;
132
- }
133
- return "";
134
- }
135
- const dshCmd = findDshBin();
136
- if (dshCmd) {
137
- const dump = quiet(`"${dshCmd}" --profile ${manifest?.profile || "web"} --dump-config`);
138
- if (dump && !/error|Error|ENOENT/i.test(dump)) {
139
- console.log("(profile dump-config 冒烟通过:DSH 配置可正常导出)");
140
- } else {
141
- console.log("(profile dump-config 冒烟未通过:请人工确认 DSH 可用后发布)");
142
- }
143
- } else {
144
- console.log("(未找到 dsh 命令,跳过 profile dump-config 冒烟)");
145
- }
146
-
147
- // ── 2. 版本 bump(package.json + README 徽章)────────────────────
148
- if (dryRun) {
149
- // 发布门禁 B1/B2(阶段 B,02 v1.4):dry-run 同样先过门禁(失败即中止,不进入授权/发布)
150
- console.log("\n=== 发布门禁 B1(README 版本四性)===");
151
- run(`cd /d "${dir}" && node scripts/readme-version-check.mjs`);
152
- console.log("\n=== 发布门禁 B2(lib/ 本机痕迹扫描)===");
153
- run(`cd /d "${dir}" && node scripts/local-residue-scan.mjs`);
154
- console.log("(发布门禁 B1/B2 通过)");
155
- console.log(`[DRY-RUN] 不修改任何文件。`);
156
- console.log(`[DRY-RUN] oldVer=${oldVer} → nextVer=${nextVer}`);
157
- const rd = join(dir, "README.md");
158
- if (existsSync(rd)) {
159
- const t = readFileSync(rd, "utf8");
160
- const oldBadgeVer = `version-${oldVer}`;
161
- const oldBadgeUrl = `version-${oldVer.replace(/-/g, "--")}`;
162
- const nextUrl = `version-${nextVer}`;
163
- const replaced = t.replaceAll(oldBadgeVer, nextUrl).replaceAll(oldBadgeUrl, nextUrl);
164
- console.log(`[DRY-RUN] README 徽章将变为: ${replaced.match(/version-[^\s]+/)?.[0] || "(未命中,检查徽章形态)"}`);
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 四点同步: 徽章 / 正文当前版本 / 历史表新行 / 固定源占位注释`);
170
- process.exit(0);
171
- }
172
- writeFileSync(pkgPath, readFileSync(pkgPath, "utf8").replace(`"version": "${oldVer}"`, `"version": "${nextVer}"`));
173
- for (const readme of ["README.md", "README.en.md"]) {
174
- const rp = join(dir, readme);
175
- if (existsSync(rp)) {
176
- const text = readFileSync(rp, "utf8");
177
- // 2026-08-29 修复:徽章 URL 编码把 `0.5.11-pre` 转成 `0.5.11--pre`(`-` 被 shields 双横线),
178
- // 旧正则 `version-0.5.11-pre` 匹配不到 `version-0.5.11--pre` → 徽章漏改(㉙ 机器根因之一)。
179
- // replaceAll 用字面串(非正则),无需转义。
180
- const oldBadgeVer = `version-${oldVer}`; // JSON 形态:version-0.5.11-pre
181
- const oldBadgeUrl = `version-${oldVer.replace(/-/g, "--")}`; // URL 形态:version-0.5.11--pre
182
- const nextUrl = `version-${nextVer}`;
183
- const replaced = text
184
- .replaceAll(oldBadgeVer, nextUrl)
185
- .replaceAll(oldBadgeUrl, nextUrl);
186
- if (replaced !== text) writeFileSync(rp, replaced);
187
- }
188
- }
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 徽章 + 正文当前版本 + 固定源占位)");
202
-
203
- // B1(2026-09-03):README 版本表行自动插入(阶段 C 人脑核对已失效的自动化;幂等:已存在 nextVer 行则跳过)
204
- // 位置:版本表(| 版本 | 日期 | 要点 |)表头后第一数据行之前(最新在上);内容用调用方 notes 参数或 git log 最近提交信息
205
- const notes = process.argv.includes("--notes") ? process.argv[process.argv.indexOf("--notes") + 1] : "";
206
- for (const readme of ["README.md"]) {
207
- const rd = join(dir, readme);
208
- if (!existsSync(rd)) continue;
209
- let rtxt = readFileSync(rd, "utf8");
210
- const tableHeader = "| 版本 | 日期 | 要点 |";
211
- const hi = rtxt.indexOf(tableHeader);
212
- if (hi < 0 || rtxt.includes(`| **${nextVer}** |`)) continue;
213
- const today = new Date().toLocaleDateString("en-CA"); // 本地时区 YYYY-MM-DD
214
- const summary = notes || (quiet(`cd /d "${dir}" && git log -1 --pretty=%s`).slice(0, 90) || "(待补变更摘要)");
215
- const nl = rtxt.indexOf("\n", hi);
216
- if (nl < 0) continue;
217
- const row = `| **${nextVer}** | ${today} | ${summary.replace(/\|/g, "\\|")} |`;
218
- rtxt = rtxt.slice(0, nl + 1) + row + "\n" + rtxt.slice(nl + 1);
219
- writeFileSync(rd, rtxt, "utf8");
220
- console.log(`[B1] ${readme} 版本表已插入 ${nextVer} 行(摘要来源:${notes ? "notes 参数" : "git log"});请核对内容`);
221
- }
222
-
223
- // ── 3. 测试(规则 23:发布前运行时验证)─────────────────────────
224
- if (pkg.scripts && pkg.scripts.test) {
225
- console.log("\n=== 运行测试 ===");
226
- run(`cd /d "${dir}" && npm test --prefix "${dir}"`);
227
- } else {
228
- console.log("(无 test 脚本,跳过)");
229
- }
230
-
231
- // ── 3.5 发布门禁 B1/B2(阶段 B,02 v1.4:测试全过后、进入授权/发布前;失败即中止)──
232
- console.log("\n=== 发布门禁 B1(README 版本四性)===");
233
- run(`cd /d "${dir}" && node scripts/readme-version-check.mjs`);
234
- console.log("\n=== 发布门禁 B2(lib/ 本机痕迹扫描)===");
235
- run(`cd /d "${dir}" && node scripts/local-residue-scan.mjs`);
236
- // ── 3.6 存在性扫描(泄露预防,2026-09-05):本机增强门禁——REAL_PATHS_SCAN 指向存在性扫描器 →
237
- // 0 命中才继续;未设置=WARN(本机工具不进包,通用用户无此工具;发布流水线建议设置)──
238
- const scanReal = process.env.REAL_PATHS_SCAN;
239
- if (scanReal) {
240
- console.log("\n=== 存在性扫描(真实路径判据,0 命中红线)===");
241
- run(`node "${scanReal}" --root "${dir}"`);
242
- } else {
243
- console.log("(未设置 REAL_PATHS_SCAN——存在性扫描跳过(本机增强门禁,建议发布前设置))");
244
- }
245
- console.log("(发布门禁 B1/B2/存在性 通过)");
246
-
247
- // ── 3.7 判据库回归(窗口 B 接入,2026-09-06):净化判据(L1 敏感串全集全历史 0 非白名单 + L2 根段未覆盖 0)
248
- // 本机增强门禁——JUDGE_SCAN 指向本机判定门禁(stage1 判据库扫描器包装);未设置=WARN ──
249
- const judgeScan = process.env.JUDGE_SCAN;
250
- if (judgeScan) {
251
- console.log("\n=== 判据库回归(净化判据,0 非白名单 / 0 未覆盖 红线)===");
252
- run(`node "${judgeScan}" --root "${dir}"`);
253
- } else {
254
- console.log("(未设置 JUDGE_SCAN——判据库回归跳过(本机增强门禁,建议发布前设置))");
255
- }
256
-
257
- // ── 3.8 豁免预插(E7,2026-09-08 接线;单源 scripts/lib/pnpm-exempt.mjs):bump 后 publish 前,
258
- // 把 nextVer 自动预插 profiles/web pnpm-workspace.yaml 的 minimumReleaseAgeExclude——⑬ 绝对口径
259
- // 要求"publish 后 latest ∈ 豁免名单",预插保证该不变式由发布器维护(防手工遗忘 → 发布后 ⑬ 红)。
260
- // 幂等(已存在跳过);DRY-RUN 只打印;yaml 读取失败抛错中止发布(fail-closed)。──
261
- {
262
- const dshHome = process.env.DSH_HOME || join(process.env.USERPROFILE || "", ".dsh");
263
- const yamlPath = join(dshHome, "profiles", "web", "pnpm-workspace.yaml");
264
- console.log("\n=== 豁免预插(minimumReleaseAgeExclude ← nextVer)===");
265
- ensureExempt(yamlPath, name, nextVer, { dryRun: !!process.env.DRY_RUN, log: console.log });
266
- }
267
-
268
- // ── 4. pack + publish ────────────────────────────────────────────
269
- console.log("\n=== npm pack ===");
270
- run(`cd /d "${dir}" && npm pack --pack-destination .`);
271
- const tgz = `${name}-${nextVer}.tgz`;
272
- if (!existsSync(join(dir, tgz))) fail(`打包产物缺失:${tgz}`);
273
-
274
- // ── 4.5 发布物存在性扫描(泄露预防硬项,2026-09-05):解包 tgz → 判据库直扫(0 命中才 publish)──
275
- const scanReal2 = process.env.REAL_PATHS_SCAN;
276
- if (scanReal2) {
277
- const unpackDir = join(dir, ".pkg-check-" + nextVer);
278
- if (existsSync(unpackDir)) rmSync(unpackDir, { recursive: true, force: true });
279
- mkdirSync(unpackDir, { recursive: true });
280
- run(`tar -xzf "${join(dir, tgz)}" -C "${unpackDir}"`);
281
- console.log("\n=== 发布物存在性扫描(解包直扫,0 命中红线)===");
282
- run(`node "${scanReal2}" --root "${join(unpackDir, "package")}"`);
283
- console.log("(发布物存在性扫描通过)");
284
- }
285
-
286
- console.log("\n=== npm publish ===");
287
- run(`cd /d "${dir}" && ${proxyPrefix()}npm publish ${tgz}`);
288
-
289
- // B1(2026-09-03):publish 后校验 registry dist-tags(0.5.16 事故:publish 自报成功但 latest 未切;
290
- // 脚本此前只跑 publish 不校验发布态——三通道验证铁律:npm 通道以 dist-tags 为准,self-report 不算数)
291
- // B1 v2(2026-09-03 同日晚):registry 传播以分钟计,publish 后立即校验曾误报中止(0.5.17 实弹)→
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} 次)===`);
296
- function sleepMs(ms) { Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms); }
297
- const queryTags = () => quiet(`cd /d "${dir}" && ${proxyPrefix()}npm view ${name} dist-tags.latest`);
298
- const queryVer = () => quiet(`cd /d "${dir}" && ${proxyPrefix()}npm view ${name}@${nextVer} version`);
299
- let latestActual = "";
300
- for (let i = 0; i < attempts; i++) {
301
- latestActual = queryTags();
302
- if (latestActual === nextVer) break;
303
- if (i < attempts - 1) { console.log(` [B1] 等待 registry 传播(${i + 1}/${attempts - 1},latest=${latestActual || "(空)"})…`); sleepMs(10000); }
304
- }
305
- if (latestActual !== nextVer) {
306
- const verExists = queryVer() === nextVer;
307
- fail(`npm 发布态异常:dist-tags.latest=${latestActual || "(为空)"},预期 ${nextVer}——${verExists ? `版本已存在但 latest 未跟:可执行 npm dist-tag add ${name}@${nextVer} latest 修正` : "版本未查询到:可能 staged/缓存(见踩坑 117)"};人工核查前勿继续 git/Release 通道`);
308
- }
309
- console.log(`dist-tags.latest=${latestActual} ✓`);
310
-
311
- // ── 5. git commit + push(token 经环境变量注入 URL,命令文本不含密钥明文)─────
312
- console.log("\n=== git commit + push ===");
313
- // 2026-08-25 修复 pushUrl 双重拼接:origin 可能是 ssh(git@github.com:)、https(https://github.com/)
314
- // 或裸路径(user/repo)三种形态——统一归一化为 "owner/repo" 路径再拼 token URL(此前 https origin 未剥前缀
315
- // 导致 "https://github.com/https://github.com/..." 双重 URL,push 404)
316
- const repo = (repoOverride || quiet(`cd /d "${dir}" && git remote get-url origin`))
317
- .replace(/^git@github\.com:/, "")
318
- .replace(/^https?:\/\/github\.com\//, "")
319
- .replace(/\.git$/, "");
320
- const token = quiet("gh auth token");
321
- if (!token) fail("无法获取 gh token");
322
- // 与 v3.72 同款通道:token 拼进 HTTPS URL 直推(避开沙箱下 msys 凭据管道的 EPERM)
323
- // 0.6.0:用户名从 repo 归属提取(不再硬编码——发布物不含本机账号)
324
- const repoOwner = repo.split("/")[0];
325
- if (!repoOwner) fail("无法解析仓库 owner(repo=" + repo + ")");
326
- const pushUrl = `https://${repoOwner}:${token}@github.com/${repo}.git`;
327
- // 2026-08-31 修复:repoRoot 实测 git 仓库根(插件目录可能只是子目录,如 rules-manager 在 oss 仓库内);
328
- // stageSpec = 插件目录相对仓库根路径(防 git add -A 误带仓库内无关改动/未跟踪物)
329
- const repoRoot = quiet(`cd /d "${dir}" && git rev-parse --show-toplevel`).trim();
330
- const relDir = relative(repoRoot, dir).split(/[\\/]/).join("/");
331
- const stageSpec = relDir && relDir !== "." ? relDir : "";
332
- // 2026-08-29 防漏机检(0.5.10 git 欠账事故:git add -u 只更新已跟踪文件,未跟踪新文件
333
- // (npm pack 按目录打包、天然包含)会漏进 git 提交 → git 与 npm 内容不一致)。
334
- // 现在:提交前检测未跟踪文件并打印名单,改用 add -A 一并提交(.gitignore 已排除杂质)。
335
- const untrackedList = quiet(`cd /d "${repoRoot}" && git status --porcelain -- ${stageSpec}`)
336
- .split("\n").filter((l) => l.startsWith("??")).map((l) => l.slice(3));
337
- if (untrackedList.length > 0) {
338
- console.log(`(未跟踪文件 ${untrackedList.length} 个将一并提交:${untrackedList.slice(0, 6).join(", ")}${untrackedList.length > 6 ? " 等" : ""})`);
339
- }
340
- // 2026-09-04:提交物本机路径/个人标识检查已本机化(release-gate.mjs → scan-real-paths.mjs
341
- // 存在性判据 + 本机词表);本文件不再内置扫描词表(发布物零个人化字符串),
342
- // 由发布方在发布前调用本机 gate 一次完成。教训 8105d3e 由本机 gate 兜底。
343
- run(`cd /d "${repoRoot}" && ${proxyPrefix()}git add -A -- ${stageSpec}`);
344
- run(`cd /d "${repoRoot}" && git -c core.autocrlf=false commit -m "release: ${name} v${nextVer}" || exit 0`);
345
- run(`cd /d "${repoRoot}" && ${proxyPrefix()}git push "${pushUrl}" HEAD`);
346
-
347
- // ── 6. GitHub Release(带正式 tgz asset,规则 26;清单标记 skipRelease 的包不建)─────
348
- if (entry?.skipRelease) {
349
- console.log("(清单标记 skipRelease,跳过 GitHub Release)");
350
- } else {
351
- console.log("\n=== GitHub Release ===");
352
- const notesFile = join(process.env.TEMP || ".", `notes-${name}-${nextVer}.md`);
353
- writeFileSync(notesFile, `## v${nextVer}\n\nRelease generated by scripts/release-plugin.mjs\n`);
354
- // 必须在插件目录(git 仓库)内运行:gh release 内部会做 git 检查
355
- run(`cd /d "${dir}" && ${proxyPrefix()}gh release create v${nextVer} "${join(dir, tgz)}" --title "${name} v${nextVer}" --notes-file "${notesFile}"`);
356
- }
357
-
358
- // ── 7. 本机 profile 同步(--sync-profile:豁免名单 + 装配审计;需 danger-full-access 运行)──
359
- if (syncProfile) {
360
- console.log("\n=== 本机 profile 同步 ===");
361
- let manifest = null;
362
- try {
363
- manifest = JSON.parse(readFileSync(join(SCRIPT_DIR, "plugins.json"), "utf8"));
364
- } catch {
365
- manifest = { profile: "web" };
366
- }
367
- const profileDir = resolve(WORKSPACE, "..", ".dsh", "profiles", manifest.profile || "web");
368
- const wsYaml = join(profileDir, "pnpm-workspace.yaml");
369
- if (existsSync(wsYaml)) {
370
- const raw = readFileSync(wsYaml, "utf8");
371
- const lineRe = new RegExp(`^( - ${name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}@[^\\n]*)$`, "m");
372
- const m = raw.match(lineRe);
373
- if (m && !m[1].includes(nextVer)) {
374
- const bakDir = join(profileDir, "..", ".backups");
375
- mkdirSync(bakDir, { recursive: true });
376
- const stamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
377
- const bak = join(bakDir, `pnpm-workspace-${stamp}.yaml`);
378
- copyFileSync(wsYaml, bak);
379
- const updated = raw.replace(lineRe, `${m[1]} || ${nextVer}`);
380
- writeFileSync(wsYaml, updated);
381
- console.log(`豁免名单已追加 ${name}@${nextVer}(备份 ${bak})`);
382
- } else if (!m) {
383
- console.log(`(豁免名单无 ${name} 条目,跳过)`);
384
- } else {
385
- console.log(`(豁免名单已含 ${nextVer},跳过)`);
386
- }
387
- }
388
- // 全量装配审计(规则 27)
389
- const auditScript = join(SCRIPT_DIR, "..", "projects", "oss", "dsh-rule-engine", "scripts", "audit-mount-consistency.mjs");
390
- if (existsSync(auditScript)) {
391
- const out = quiet(`node "${auditScript}" --profile ${manifest.profile || "web"}`);
392
- const pass = /MOUNT CONSISTENT/.test(out);
393
- console.log(out.split("\n").filter((l) => /RESULT|DUPLICATES|MOUNT|summary/.test(l)).join("\n"));
394
- if (!pass) fail("装配审计未通过(MOUNT CONSISTENT 未出现),请先处理再重启 DSH");
395
- console.log("装配审计 MOUNT CONSISTENT ✓");
396
- }
397
- }
398
-
399
- console.log(`\n=== 发布完成:${name} v${nextVer} ===`);
400
- console.log(`npm: ${name}@${nextVer}`);
401
- console.log(`release: https://github.com/${repo}/releases/tag/v${nextVer}`);
@@ -1,55 +0,0 @@
1
- // rules-health.mjs - 规则触发率统计(阶段 2"数据驱动精简"的基础工具,2026-08-24)
2
- // 用法:node scripts/rules-health.mjs [--top 10]
3
- // 数据源:DSH_HOME/rule-engine.log.jsonl(引擎审计日志——四类审计自 2026-08-24 起)
4
- // 输出:① 各规则触发统计(按 total 排序)② 零命中清单(对照 rule-understanding.json 规则全集)
5
- // ③ 样本标注:总量 < MIN_SAMPLE 时提示"样本不足,暂不据此删/并/浓缩规则"。
6
- import { readFileSync, existsSync } from "node:fs";
7
- import { join } from "node:path";
8
- import os from "node:os";
9
-
10
- const DSH_HOME = process.env.DSH_HOME || join(os.homedir(), ".dsh");
11
- const LOG = join(DSH_HOME, "rule-engine.log.jsonl");
12
- const UNDERSTANDING = join(DSH_HOME, "rule-understanding.json");
13
- const MIN_SAMPLE = 200;
14
- const TOP = Number((process.argv.find((a) => a.startsWith("--top=")) || "").split("=")[1]) || 10;
15
-
16
- if (!existsSync(LOG)) {
17
- console.log(`NO LOG: ${LOG}`);
18
- process.exit(0);
19
- }
20
-
21
- const stats = new Map(); // ruleId -> Map(kind -> count)
22
- let total = 0;
23
- for (const line of readFileSync(LOG, "utf8").split("\n")) {
24
- if (!line.trim()) continue;
25
- let e;
26
- try { e = JSON.parse(line); } catch { continue; }
27
- if (!e || !e.kind || !e.rule) continue;
28
- total++;
29
- const byKind = stats.get(String(e.rule)) || new Map();
30
- byKind.set(e.kind, (byKind.get(e.kind) || 0) + 1);
31
- stats.set(String(e.rule), byKind);
32
- }
33
-
34
- // 规则全集(对照零命中)
35
- let ruleIds = new Set();
36
- try {
37
- const u = JSON.parse(readFileSync(UNDERSTANDING, "utf8"));
38
- ruleIds = new Set((u.rules || []).map((r) => String(r.ruleId)));
39
- } catch { /* 理解产物缺失时只统计日志内规则 */ }
40
-
41
- const rows = [...stats.entries()]
42
- .map(([rule, byKind]) => ({ rule, total: [...byKind.values()].reduce((a, b) => a + b, 0), byKind }))
43
- .sort((a, b) => b.total - a.total);
44
-
45
- console.log(`规则触发率统计(样本总量 ${total}${total < MIN_SAMPLE ? `,⚠ 样本不足 ${MIN_SAMPLE}——暂不据此删/并规则` : ""})`);
46
- console.log("规则 触发数 分类明细");
47
- for (const { rule, total, byKind } of rows.slice(0, TOP)) {
48
- const detail = [...byKind.entries()].map(([k, n]) => `${k}:${n}`).join(" ");
49
- console.log(`${String(rule).padEnd(10)} ${String(total).padEnd(7)} ${detail}`);
50
- }
51
- const zero = [...ruleIds].filter((id) => !stats.has(id));
52
- if (zero.length > 0) {
53
- console.log(`\n零命中规则(${zero.length}):${zero.join(", ")}`);
54
- console.log("注:零命中 ≠ 无用(条件触发型低频规则正常);仅作体检参考,勿据单次样本删规则。");
55
- }