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.
@@ -1,14 +0,0 @@
1
- // probe-intent.mjs - 测"落"字为何被判问句(用户明确执行却拦)
2
- import { parseUserIntents, shouldDenyMutation } from "../lib/core/intent.js";
3
- const texts = [
4
- "2、连同上面这条一起落",
5
- "连上面这条一起落",
6
- "落",
7
- "全部一起落",
8
- "认可全部link不装npm形态",
9
- "1、认可形态 2、连上面这条一起落"
10
- ];
11
- for (const t of texts) {
12
- const r = parseUserIntents(t);
13
- console.log(`"${t}" → hasExecute=${r.hasExecute} hasQuestion=${r.hasQuestion} types=${r.clauses.map((c) => c.type).join(",")}`);
14
- }
@@ -1,105 +0,0 @@
1
- // publish-aptitude-check.mjs — 发布适用性门禁(T6,2026-08-31)
2
- // 目的:兑现 README「通用化」承诺的"陌生人视角"检查——发布物必须先证明:
3
- // ① 无 AGENTS.md(陌生环境冷启动):引擎零错 / 零死映射 / 零意外拦截面
4
- // ② 空白规则文件:静默(0 规则、无错、无 dead)
5
- // ③ 任意编号规则(非本机编号):声明式绑定与"纯自证"分流行为正确
6
- // ④ 四要素 + 执行等级格式的最小规则:actions/handler 解析正确(README 格式契约冒烟)
7
- // ⑤ 发布物个人标识扫描:files 白名单内文件不得含本机个人标识(本地用户名/真实路径/邮箱/私人注释)
8
- // 默认模式 = 单进程纯函数链(不 spawn、不 import index.js,沙箱可直接跑);
9
- // --deep 模式 = 额外子进程全链冷启动(临时 DSH_HOME + 完整引擎加载 + 真实裁决链,需完整权限/CI)。
10
- // 用法:node scripts/publish-aptitude-check.mjs [--deep] [--tgz <路径>]
11
- import { mkdtempSync, writeFileSync, rmSync, readFileSync, readdirSync, statSync, existsSync } from "node:fs";
12
- import { tmpdir } from "node:os";
13
- import { join, dirname, resolve } from "node:path";
14
- import { fileURLToPath } from "node:url";
15
-
16
- const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
17
- const args = process.argv.slice(2);
18
- const deep = args.includes("--deep");
19
- const tgzArg = args.includes("--tgz") ? args[args.indexOf("--tgz") + 1] : null;
20
-
21
- const FAILS = [];
22
- function check(ok, msg) {
23
- console.log(`${ok ? "✅" : "❌"} ${msg}`);
24
- if (!ok) FAILS.push(msg);
25
- }
26
-
27
- // ①-④ 使用的纯函数链(与引擎 lib 同源,动态 import 保证读到本次代码)
28
- const { loadRules } = await import("../lib/core/parser.js");
29
- const { understandAll, understandRule, analyzeCoverage, defaultHandlerMapSize } = await import("../lib/core/understander.js");
30
-
31
- // ── ① 陌生环境冷启动(临时 DSH_HOME,无 AGENTS.md)──
32
- const sceneDir = mkdtempSync(join(tmpdir(), "aptitude-check-"));
33
- process.env.DSH_HOME = sceneDir;
34
- process.env.DSH_WORKSPACE = root;
35
- {
36
- const parsed = loadRules();
37
- check(parsed.ok === false && parsed.missing === true, `① 无 AGENTS.md:处理器返回 missing(ok=${parsed.ok}, missing=${parsed.missing})`);
38
- const configs = understandAll([]);
39
- check(configs.length === 0, "① 无 AGENTS.md:理解产物为空(不产生任何规则)");
40
- const { dead, uncovered } = analyzeCoverage(configs);
41
- // 残余1 剥离后:代码层无默认表(通用部署空表)→ dead=0;关键断言 = 无规则被声称机器执行(uncovered=0)且冷启动零报错
42
- check(dead.size === 0, `① 无 AGENTS.md:dead=0(代码层无默认偏好表)`);
43
- check(uncovered.length === 0, `① 无 AGENTS.md:零未覆盖(uncovered=0)`);
44
- }
45
-
46
- // ── ② 空白规则文件静默 ──
47
- {
48
- writeFileSync(join(sceneDir, "AGENTS.md"), "", "utf8");
49
- const parsed = loadRules();
50
- check(parsed.ok === true && parsed.rules.length === 0, `② 空白 AGENTS.md:解析成功且 0 规则(rules=${parsed.rules.length})`);
51
- const configs = understandAll(parsed.rules);
52
- check(configs.length === 0, "② 空白 AGENTS.md:理解产物为空");
53
- }
54
-
55
- // ── ③ 任意编号规则(非本机编号):声明式绑定 + 纯自证分流 ──
56
- {
57
- const arbitrary = {
58
- index: "X-77",
59
- title: "陌生用户规则(执行等级:A+D)",
60
- section: "自定义分区",
61
- level: "A+D",
62
- body: "- **触发**:使用内联命令。\n- **检查**:拦 node -e / pwsh -c。\n- **动作**:拒绝。\n- **豁免**:无。"
63
- };
64
- const plain = understandRule(arbitrary);
65
- check(plain.handler === "", "③ 任意编号 + 无声明:handler 为空(纯自证分流,不误绑定本机执行器)");
66
- check(plain.actions.includes("deny") && plain.actions.includes("self-certify"), "③ 任意编号:A+D 动作解析正确(deny+self-certify)");
67
- // 架构 v3 P1:声明值统一归一为 kind 名——kind 名(陌生用户视角)与历史内部名(兼容)都须绑定
68
- const declaredKind = understandRule({ ...arbitrary, body: arbitrary.body + "\n<!-- handler: approval -->" });
69
- check(declaredKind.handler === "approval", "③ 任意编号 + 声明(kind 名):绑定指定执行器");
70
- const declaredLegacy = understandRule({ ...arbitrary, body: arbitrary.body + "\n<!-- handler: rule12a-approval -->" });
71
- check(declaredLegacy.handler === "approval", "③ 任意编号 + 声明(历史内部名):归一为同一 kind");
72
- const covered = analyzeCoverage(understandAll([arbitrary]));
73
- check(covered.uncovered.some((u) => u.ruleId === "X-77"), "③ 任意编号 A 级无 handler:被标记未覆盖(提示正确)");
74
- // 通用化闭环:defaultMap 清空 → 陌生规则无兜底绑定(纯声明式世界,不借道本机偏好表);
75
- // 注:analyzeCoverage 自省对象恒为本机默认表(体检对象),不受 defaultMap 影响。
76
- const bare = understandRule(arbitrary, { defaultMap: {} });
77
- check(bare.handler === "", "③ defaultMap 清空:陌生规则无兜底绑定(纯声明式世界)");
78
- }
79
-
80
- // ── ④ 四要素 + 执行等级格式契约冒烟(README 格式)──
81
- {
82
- const elem = understandRule({
83
- index: "E-1",
84
- title: "格式样例(执行等级:B + D)",
85
- section: "样例分区",
86
- level: "B + D",
87
- body: "- **触发**:触发条件。\n- **检查**:检查项。\n- **动作**:动作说明。\n- **豁免**:豁免说明。"
88
- });
89
- check(elem.elements.trigger.length > 0 && elem.elements.check.length > 0 && elem.elements.action.length > 0 && elem.elements.exemption.length > 0, "④ 四要素提取完整");
90
- check(elem.actions.includes("correct") && elem.actions.includes("self-certify"), "④ 组合等级 B + D 动作解析正确");
91
- check(elem.confidence === "high", "④ 四要素齐全 → 置信 high");
92
- }
93
-
94
- // ── ⑤(2026-09-04 本机化:存在性判据 + 个人词表扫描移入本机发行工具
95
- // scripts/scan-real-paths.mjs(判据库 scripts/lib/realpath-guard.js),
96
- // 本文件只保留 ①-④ 通用发布适用性检查;发布前由本机 release-gate 统一执行门禁)──
97
-
98
- // ── --deep:完整引擎链冷启动(子进程,需完整权限/CI;沙箱不可用)──
99
- if (deep) {
100
- console.log("\n(--deep 模式:请确认在完整权限/CI 环境运行——子进程捕获受沙箱限制)");
101
- }
102
-
103
- rmSync(sceneDir, { recursive: true, force: true });
104
- console.log(`\n${FAILS.length === 0 ? "ADEPTITUDE CHECK PASS" : `ADEPTITUDE CHECK FAIL(${FAILS.length} 项)`}`);
105
- process.exit(FAILS.length === 0 ? 0 : 1);
@@ -1,41 +0,0 @@
1
- #!/usr/bin/env node
2
- // readme-version-check.mjs — README 版本四性一致性门禁
3
- // 检查:package.json version == README 徽章 == README 正文"当前版本 X" == 版本历史表含当前版本 == 发行固定源锚定当前版本
4
- // 用法:node scripts/readme-version-check.mjs (在包根目录运行;exit 0 = 一致,exit 1 = 不一致)
5
-
6
- import { readFileSync } from "node:fs";
7
- import { fileURLToPath } from "node:url";
8
- import { dirname, join } from "node:path";
9
-
10
- const root = join(dirname(fileURLToPath(import.meta.url)), "..");
11
- const pkg = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
12
- const readme = readFileSync(join(root, "README.md"), "utf8");
13
- const ver = pkg.version;
14
-
15
- let failures = [];
16
- const ok = (cond, label, detail) => {
17
- if (cond) console.log(`PASS ${label}`);
18
- else { failures.push(label); console.log(`FAIL ${label} — ${detail}`); }
19
- };
20
-
21
- // 1. 徽章:badge/version-X.Y.Z
22
- const badge = readme.match(/badge\/version-(\d+\.\d+\.\d+)/);
23
- ok(badge?.[1] === ver, "徽章=package.json", `徽章=${badge?.[1] ?? "未找到"} vs package=${ver}`);
24
-
25
- // 2. 正文"当前版本 X"(允许"当前版本 **X**")
26
- const cur = readme.match(/当前版本[^\d]{0,10}(\d+\.\d+\.\d+)/);
27
- ok(cur?.[1] === ver, "正文当前版本=package.json", `正文=${cur?.[1] ?? "未找到"} vs package=${ver}`);
28
-
29
- // 3. 版本历史表包含当前版本行:| **X.Y.Z** | 或 | X.Y.Z |
30
- const tableHit = new RegExp(`^\\|\\s*\\*{0,2}${ver.replace(/\./g, "\\.")}\\*{0,2}\\s*\\|`, "m").test(readme);
31
- ok(tableHit, "版本历史表含当前版本行", `未找到 | ${ver} | 行`);
32
-
33
- // 4. 发行固定源锚定当前版本:"**X(当前)**" 或 "X.Y.Z(当前)"
34
- const pin = readme.match(/\*\*(\d+\.\d+\.\d+)(当前)\*\*/);
35
- ok(pin?.[1] === ver, "发行固定源=package.json", `固定源=${pin?.[1] ?? "未找到"} vs package=${ver}`);
36
-
37
- if (failures.length) {
38
- console.error(`\nVERSION CHECK FAILED(${failures.length} 项不一致)`);
39
- process.exit(1);
40
- }
41
- console.log(`\nVERSION CHECK OK(${ver} 四处一致)`);
@@ -1,432 +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
- console.log(`[DRY-RUN] 固定源将更新为: **${nextVer}(当前)** 锚点 ${quiet(`cd /d "${dir}" && git rev-parse HEAD`).trim().slice(0, 7)}(发布后自动回填 release 提交号)`);
171
- process.exit(0);
172
- }
173
- writeFileSync(pkgPath, readFileSync(pkgPath, "utf8").replace(`"version": "${oldVer}"`, `"version": "${nextVer}"`));
174
- for (const readme of ["README.md", "README.en.md"]) {
175
- const rp = join(dir, readme);
176
- if (existsSync(rp)) {
177
- const text = readFileSync(rp, "utf8");
178
- // 2026-08-29 修复:徽章 URL 编码把 `0.5.11-pre` 转成 `0.5.11--pre`(`-` 被 shields 双横线),
179
- // 旧正则 `version-0.5.11-pre` 匹配不到 `version-0.5.11--pre` → 徽章漏改(㉙ 机器根因之一)。
180
- // replaceAll 用字面串(非正则),无需转义。
181
- const oldBadgeVer = `version-${oldVer}`; // JSON 形态:version-0.5.11-pre
182
- const oldBadgeUrl = `version-${oldVer.replace(/-/g, "--")}`; // URL 形态:version-0.5.11--pre
183
- const nextUrl = `version-${nextVer}`;
184
- const replaced = text
185
- .replaceAll(oldBadgeVer, nextUrl)
186
- .replaceAll(oldBadgeUrl, nextUrl);
187
- if (replaced !== text) writeFileSync(rp, replaced);
188
- }
189
- }
190
- // §1.2-②(2026-09-08 第二批):README 正文"当前版本"行同步
191
- // 方案 A(2026-09-09,踩坑 140 根治):固定源行**自动维护**——bump 时插入「(当前)」行(锚点=改号前 HEAD),
192
- // 发布提交后由 §5.5 回填真实 release 提交号;旧「(当前)」标记与遗留占位注释一并清理。
193
- // 旧做法只插占位注释,而 B1 门禁要求「固定源=package 版本」→ 每次发布必撞(0.6.3 首发实证)。
194
- const headBeforeShort = quiet(`cd /d "${dir}" && git rev-parse HEAD`).trim().slice(0, 7);
195
- let fixedSourceBumpLine = "";
196
- for (const readme of ["README.md", "README.en.md"]) {
197
- const rp = join(dir, readme);
198
- if (!existsSync(rp)) continue;
199
- let rtxt = readFileSync(rp, "utf8");
200
- const before = rtxt;
201
- rtxt = rtxt.replace(/(> 当前版本 \*\*)\d+\.\d+\.\d+(\*\*)/, `$1${nextVer}$2`);
202
- if (readme === "README.md" && rtxt.includes("## 发行固定源")) {
203
- // ① 旧「(当前)」标记降级(只保留版本号加粗)
204
- rtxt = rtxt.replace(/\*\*(\d+\.\d+\.\d+)(当前)\*\*/g, "**$1**");
205
- // ② 清理遗留占位注释(方案 A 起不再产生)
206
- rtxt = rtxt.replace(/\n<!-- fixed-source: [^>]*-->\n/g, "\n");
207
- // ③ 插入新版本行(幂等:已存在则跳过)
208
- if (!rtxt.includes(`- **${nextVer}(当前)**`)) {
209
- fixedSourceBumpLine = `- **${nextVer}(当前)** 固定于 main Commit \`${headBeforeShort}\`(\`git checkout ${headBeforeShort}\` + 发布 bump 可复现 npm \`${name}@${nextVer}\` 与 GitHub Release v${nextVer} 同源代码)。**注:release 提交号由发布脚本自动回填。**`;
210
- rtxt = rtxt.replace("## 发行固定源\n", `## 发行固定源\n\n${fixedSourceBumpLine}\n`);
211
- }
212
- }
213
- if (rtxt !== before) writeFileSync(rp, rtxt);
214
- }
215
- console.log(`版本已 bump(package.json + README 徽章 + 正文当前版本 + 固定源 ${nextVer}(锚点 ${headBeforeShort}))`);
216
-
217
- // B1(2026-09-03):README 版本表行自动插入(阶段 C 人脑核对已失效的自动化;幂等:已存在 nextVer 行则跳过)
218
- // 位置:版本表(| 版本 | 日期 | 要点 |)表头后第一数据行之前(最新在上);内容用调用方 notes 参数或 git log 最近提交信息
219
- const notes = process.argv.includes("--notes") ? process.argv[process.argv.indexOf("--notes") + 1] : "";
220
- for (const readme of ["README.md"]) {
221
- const rd = join(dir, readme);
222
- if (!existsSync(rd)) continue;
223
- let rtxt = readFileSync(rd, "utf8");
224
- const tableHeader = "| 版本 | 日期 | 要点 |";
225
- const hi = rtxt.indexOf(tableHeader);
226
- if (hi < 0 || rtxt.includes(`| **${nextVer}** |`)) continue;
227
- const today = new Date().toLocaleDateString("en-CA"); // 本地时区 YYYY-MM-DD
228
- const summary = notes || (quiet(`cd /d "${dir}" && git log -1 --pretty=%s`).slice(0, 90) || "(待补变更摘要)");
229
- const nl = rtxt.indexOf("\n", hi);
230
- if (nl < 0) continue;
231
- const row = `| **${nextVer}** | ${today} | ${summary.replace(/\|/g, "\\|")} |`;
232
- rtxt = rtxt.slice(0, nl + 1) + row + "\n" + rtxt.slice(nl + 1);
233
- writeFileSync(rd, rtxt, "utf8");
234
- console.log(`[B1] ${readme} 版本表已插入 ${nextVer} 行(摘要来源:${notes ? "notes 参数" : "git log"});请核对内容`);
235
- }
236
-
237
- // ── 3. 测试(规则 23:发布前运行时验证)─────────────────────────
238
- if (pkg.scripts && pkg.scripts.test) {
239
- console.log("\n=== 运行测试 ===");
240
- run(`cd /d "${dir}" && npm test --prefix "${dir}"`);
241
- } else {
242
- console.log("(无 test 脚本,跳过)");
243
- }
244
-
245
- // ── 3.5 发布门禁 B1/B2(阶段 B,02 v1.4:测试全过后、进入授权/发布前;失败即中止)──
246
- console.log("\n=== 发布门禁 B1(README 版本四性)===");
247
- run(`cd /d "${dir}" && node scripts/readme-version-check.mjs`);
248
- console.log("\n=== 发布门禁 B2(lib/ 本机痕迹扫描)===");
249
- run(`cd /d "${dir}" && node scripts/local-residue-scan.mjs`);
250
- // ── 3.6 存在性扫描(泄露预防,2026-09-05):本机增强门禁——REAL_PATHS_SCAN 指向存在性扫描器 →
251
- // 0 命中才继续;未设置=WARN(本机工具不进包,通用用户无此工具;发布流水线建议设置)──
252
- const scanReal = process.env.REAL_PATHS_SCAN;
253
- if (scanReal) {
254
- console.log("\n=== 存在性扫描(真实路径判据,0 命中红线)===");
255
- run(`node "${scanReal}" --root "${dir}"`);
256
- } else {
257
- console.log("(未设置 REAL_PATHS_SCAN——存在性扫描跳过(本机增强门禁,建议发布前设置))");
258
- }
259
- console.log("(发布门禁 B1/B2/存在性 通过)");
260
-
261
- // ── 3.7 判据库回归(窗口 B 接入,2026-09-06):净化判据(L1 敏感串全集全历史 0 非白名单 + L2 根段未覆盖 0)
262
- // 本机增强门禁——JUDGE_SCAN 指向本机判定门禁(stage1 判据库扫描器包装);未设置=WARN ──
263
- const judgeScan = process.env.JUDGE_SCAN;
264
- if (judgeScan) {
265
- console.log("\n=== 判据库回归(净化判据,0 非白名单 / 0 未覆盖 红线)===");
266
- run(`node "${judgeScan}" --root "${dir}"`);
267
- } else {
268
- console.log("(未设置 JUDGE_SCAN——判据库回归跳过(本机增强门禁,建议发布前设置))");
269
- }
270
-
271
- // ── 3.8 豁免预插(E7,2026-09-08 接线;单源 scripts/lib/pnpm-exempt.mjs):bump 后 publish 前,
272
- // 把 nextVer 自动预插 profiles/web pnpm-workspace.yaml 的 minimumReleaseAgeExclude——⑬ 绝对口径
273
- // 要求"publish 后 latest ∈ 豁免名单",预插保证该不变式由发布器维护(防手工遗忘 → 发布后 ⑬ 红)。
274
- // 幂等(已存在跳过);DRY-RUN 只打印;yaml 读取失败抛错中止发布(fail-closed)。──
275
- {
276
- const dshHome = process.env.DSH_HOME || join(process.env.USERPROFILE || "", ".dsh");
277
- const yamlPath = join(dshHome, "profiles", "web", "pnpm-workspace.yaml");
278
- console.log("\n=== 豁免预插(minimumReleaseAgeExclude ← nextVer)===");
279
- ensureExempt(yamlPath, name, nextVer, { dryRun: !!process.env.DRY_RUN, log: console.log });
280
- }
281
-
282
- // ── 4. pack + publish ────────────────────────────────────────────
283
- console.log("\n=== npm pack ===");
284
- run(`cd /d "${dir}" && npm pack --pack-destination .`);
285
- const tgz = `${name}-${nextVer}.tgz`;
286
- if (!existsSync(join(dir, tgz))) fail(`打包产物缺失:${tgz}`);
287
-
288
- // ── 4.5 发布物存在性扫描(泄露预防硬项,2026-09-05):解包 tgz → 判据库直扫(0 命中才 publish)──
289
- const scanReal2 = process.env.REAL_PATHS_SCAN;
290
- if (scanReal2) {
291
- const unpackDir = join(dir, ".pkg-check-" + nextVer);
292
- if (existsSync(unpackDir)) rmSync(unpackDir, { recursive: true, force: true });
293
- mkdirSync(unpackDir, { recursive: true });
294
- run(`tar -xzf "${join(dir, tgz)}" -C "${unpackDir}"`);
295
- console.log("\n=== 发布物存在性扫描(解包直扫,0 命中红线)===");
296
- run(`node "${scanReal2}" --root "${join(unpackDir, "package")}"`);
297
- console.log("(发布物存在性扫描通过)");
298
- }
299
-
300
- console.log("\n=== npm publish ===");
301
- run(`cd /d "${dir}" && ${proxyPrefix()}npm publish ${tgz}`);
302
-
303
- // B1(2026-09-03):publish 后校验 registry dist-tags(0.5.16 事故:publish 自报成功但 latest 未切;
304
- // 脚本此前只跑 publish 不校验发布态——三通道验证铁律:npm 通道以 dist-tags 为准,self-report 不算数)
305
- // B1 v2(2026-09-03 同日晚):registry 传播以分钟计,publish 后立即校验曾误报中止(0.5.17 实弹)→
306
- // §1.2-③(2026-09-08 第二批):轮询 60s→180s(本机首次传播实测 >60s);RELEASE_DIST_TAG_POLL_SEC 可配(下限 30)
307
- const pollSec = Math.max(30, Number(process.env.RELEASE_DIST_TAG_POLL_SEC) || 180);
308
- const attempts = Math.ceil(pollSec / 10);
309
- console.log(`\n=== npm 发布态校验(dist-tags,轮询最多 ${pollSec}s / ${attempts} 次)===`);
310
- function sleepMs(ms) { Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms); }
311
- const queryTags = () => quiet(`cd /d "${dir}" && ${proxyPrefix()}npm view ${name} dist-tags.latest`);
312
- const queryVer = () => quiet(`cd /d "${dir}" && ${proxyPrefix()}npm view ${name}@${nextVer} version`);
313
- let latestActual = "";
314
- for (let i = 0; i < attempts; i++) {
315
- latestActual = queryTags();
316
- if (latestActual === nextVer) break;
317
- if (i < attempts - 1) { console.log(` [B1] 等待 registry 传播(${i + 1}/${attempts - 1},latest=${latestActual || "(空)"})…`); sleepMs(10000); }
318
- }
319
- if (latestActual !== nextVer) {
320
- const verExists = queryVer() === nextVer;
321
- fail(`npm 发布态异常:dist-tags.latest=${latestActual || "(为空)"},预期 ${nextVer}——${verExists ? `版本已存在但 latest 未跟:可执行 npm dist-tag add ${name}@${nextVer} latest 修正` : "版本未查询到:可能 staged/缓存(见踩坑 117)"};人工核查前勿继续 git/Release 通道`);
322
- }
323
- console.log(`dist-tags.latest=${latestActual} ✓`);
324
-
325
- // ── 5. git commit + push(token 经环境变量注入 URL,命令文本不含密钥明文)─────
326
- console.log("\n=== git commit + push ===");
327
- // 2026-08-25 修复 pushUrl 双重拼接:origin 可能是 ssh(git@github.com:)、https(https://github.com/)
328
- // 或裸路径(user/repo)三种形态——统一归一化为 "owner/repo" 路径再拼 token URL(此前 https origin 未剥前缀
329
- // 导致 "https://github.com/https://github.com/..." 双重 URL,push 404)
330
- const repo = (repoOverride || quiet(`cd /d "${dir}" && git remote get-url origin`))
331
- .replace(/^git@github\.com:/, "")
332
- .replace(/^https?:\/\/github\.com\//, "")
333
- .replace(/\.git$/, "");
334
- const token = quiet("gh auth token");
335
- if (!token) fail("无法获取 gh token");
336
- // 与 v3.72 同款通道:token 拼进 HTTPS URL 直推(避开沙箱下 msys 凭据管道的 EPERM)
337
- // 0.6.0:用户名从 repo 归属提取(不再硬编码——发布物不含本机账号)
338
- const repoOwner = repo.split("/")[0];
339
- if (!repoOwner) fail("无法解析仓库 owner(repo=" + repo + ")");
340
- const pushUrl = `https://${repoOwner}:${token}@github.com/${repo}.git`;
341
- // 2026-08-31 修复:repoRoot 实测 git 仓库根(插件目录可能只是子目录,如 rules-manager 在 oss 仓库内);
342
- // stageSpec = 插件目录相对仓库根路径(防 git add -A 误带仓库内无关改动/未跟踪物)
343
- const repoRoot = quiet(`cd /d "${dir}" && git rev-parse --show-toplevel`).trim();
344
- const relDir = relative(repoRoot, dir).split(/[\\/]/).join("/");
345
- const stageSpec = relDir && relDir !== "." ? relDir : "";
346
- // 2026-08-29 防漏机检(0.5.10 git 欠账事故:git add -u 只更新已跟踪文件,未跟踪新文件
347
- // (npm pack 按目录打包、天然包含)会漏进 git 提交 → git 与 npm 内容不一致)。
348
- // 现在:提交前检测未跟踪文件并打印名单,改用 add -A 一并提交(.gitignore 已排除杂质)。
349
- const untrackedList = quiet(`cd /d "${repoRoot}" && git status --porcelain -- ${stageSpec}`)
350
- .split("\n").filter((l) => l.startsWith("??")).map((l) => l.slice(3));
351
- if (untrackedList.length > 0) {
352
- console.log(`(未跟踪文件 ${untrackedList.length} 个将一并提交:${untrackedList.slice(0, 6).join(", ")}${untrackedList.length > 6 ? " 等" : ""})`);
353
- }
354
- // 2026-09-04:提交物本机路径/个人标识检查已本机化(release-gate.mjs → scan-real-paths.mjs
355
- // 存在性判据 + 本机词表);本文件不再内置扫描词表(发布物零个人化字符串),
356
- // 由发布方在发布前调用本机 gate 一次完成。教训 8105d3e 由本机 gate 兜底。
357
- run(`cd /d "${repoRoot}" && ${proxyPrefix()}git add -A -- ${stageSpec}`);
358
- run(`cd /d "${repoRoot}" && git -c core.autocrlf=false commit -m "release: ${name} v${nextVer}" || exit 0`);
359
- run(`cd /d "${repoRoot}" && ${proxyPrefix()}git push "${pushUrl}" HEAD`);
360
-
361
- // ── 5.5 固定源回填(方案 A,2026-09-09):用 release 提交号替换 bump 时的锚点 ──
362
- if (fixedSourceBumpLine) {
363
- const relShort = quiet(`cd /d "${repoRoot}" && git rev-parse HEAD`).trim().slice(0, 7);
364
- const rd = join(dir, "README.md");
365
- const rtxt = existsSync(rd) ? readFileSync(rd, "utf8") : "";
366
- const finalLine = `- **${nextVer}(当前)** 固定于 main Commit \`${relShort}\`(\`git checkout ${relShort}\` 可复现 npm \`${name}@${nextVer}\` 与 GitHub Release v${nextVer} 同源代码)。`;
367
- if (rtxt.includes(fixedSourceBumpLine)) {
368
- writeFileSync(rd, rtxt.replace(fixedSourceBumpLine, finalLine), "utf8");
369
- console.log(`\n=== 固定源回填 ===\n${nextVer}:锚点 ${headBeforeShort} → release 提交 ${relShort}`);
370
- run(`cd /d "${repoRoot}" && ${proxyPrefix()}git add -A -- ${stageSpec}`);
371
- run(`cd /d "${repoRoot}" && git -c core.autocrlf=false commit -m "docs(readme): ${nextVer} 固定源回填 ${relShort}(发布后自动同步)" || exit 0`);
372
- run(`cd /d "${repoRoot}" && ${proxyPrefix()}git push "${pushUrl}" HEAD`);
373
- } else {
374
- console.log("(固定源回填跳过:未匹配到 bump 行——可能已手工回填)");
375
- }
376
- }
377
-
378
- // ── 6. GitHub Release(带正式 tgz asset,规则 26;清单标记 skipRelease 的包不建)─────
379
- if (entry?.skipRelease) {
380
- console.log("(清单标记 skipRelease,跳过 GitHub Release)");
381
- } else {
382
- console.log("\n=== GitHub Release ===");
383
- const notesFile = join(process.env.TEMP || ".", `notes-${name}-${nextVer}.md`);
384
- writeFileSync(notesFile, `## v${nextVer}\n\nRelease generated by scripts/release-plugin.mjs\n`);
385
- // 必须在插件目录(git 仓库)内运行:gh release 内部会做 git 检查
386
- run(`cd /d "${dir}" && ${proxyPrefix()}gh release create v${nextVer} "${join(dir, tgz)}" --title "${name} v${nextVer}" --notes-file "${notesFile}"`);
387
- }
388
-
389
- // ── 7. 本机 profile 同步(--sync-profile:豁免名单 + 装配审计;需 danger-full-access 运行)──
390
- if (syncProfile) {
391
- console.log("\n=== 本机 profile 同步 ===");
392
- let manifest = null;
393
- try {
394
- manifest = JSON.parse(readFileSync(join(SCRIPT_DIR, "plugins.json"), "utf8"));
395
- } catch {
396
- manifest = { profile: "web" };
397
- }
398
- const profileDir = resolve(WORKSPACE, "..", ".dsh", "profiles", manifest.profile || "web");
399
- const wsYaml = join(profileDir, "pnpm-workspace.yaml");
400
- if (existsSync(wsYaml)) {
401
- const raw = readFileSync(wsYaml, "utf8");
402
- const lineRe = new RegExp(`^( - ${name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}@[^\\n]*)$`, "m");
403
- const m = raw.match(lineRe);
404
- if (m && !m[1].includes(nextVer)) {
405
- const bakDir = join(profileDir, "..", ".backups");
406
- mkdirSync(bakDir, { recursive: true });
407
- const stamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
408
- const bak = join(bakDir, `pnpm-workspace-${stamp}.yaml`);
409
- copyFileSync(wsYaml, bak);
410
- const updated = raw.replace(lineRe, `${m[1]} || ${nextVer}`);
411
- writeFileSync(wsYaml, updated);
412
- console.log(`豁免名单已追加 ${name}@${nextVer}(备份 ${bak})`);
413
- } else if (!m) {
414
- console.log(`(豁免名单无 ${name} 条目,跳过)`);
415
- } else {
416
- console.log(`(豁免名单已含 ${nextVer},跳过)`);
417
- }
418
- }
419
- // 全量装配审计(规则 27)
420
- const auditScript = join(SCRIPT_DIR, "..", "projects", "oss", "dsh-rule-engine", "scripts", "audit-mount-consistency.mjs");
421
- if (existsSync(auditScript)) {
422
- const out = quiet(`node "${auditScript}" --profile ${manifest.profile || "web"}`);
423
- const pass = /MOUNT CONSISTENT/.test(out);
424
- console.log(out.split("\n").filter((l) => /RESULT|DUPLICATES|MOUNT|summary/.test(l)).join("\n"));
425
- if (!pass) fail("装配审计未通过(MOUNT CONSISTENT 未出现),请先处理再重启 DSH");
426
- console.log("装配审计 MOUNT CONSISTENT ✓");
427
- }
428
- }
429
-
430
- console.log(`\n=== 发布完成:${name} v${nextVer} ===`);
431
- console.log(`npm: ${name}@${nextVer}`);
432
- 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
- }