dsh-rule-engine 0.5.12 → 0.5.14
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 +31 -100
- package/lib/core/authorization.js +29 -5
- package/lib/core/config.js +73 -66
- package/lib/core/intent.js +21 -2
- package/lib/core/lexicon.js +1 -1
- package/lib/core/parser.js +111 -103
- package/lib/core/patterns.js +14 -1
- package/lib/core/state.js +32 -13
- package/lib/core/text-detect.js +56 -11
- package/lib/core/understander.js +221 -150
- package/lib/index.js +66 -28
- package/lib/service.js +17 -2
- package/package.json +1 -1
- package/scripts/publish-aptitude-check.mjs +144 -0
- package/scripts/release-plugin.mjs +277 -0
- package/scripts/verify-all.mjs +4 -0
package/lib/service.js
CHANGED
|
@@ -29,6 +29,19 @@ function currentVersion() {
|
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
/** 仓库 slug(owner/repo):从 package.json repository 解析——通用化后不硬编码作者仓库;
|
|
33
|
+
* 空 = 无法解析(发布包必带 repository,正常路径;异常时检查功能不可用,fail-closed 无害)。 */
|
|
34
|
+
function repoSlug() {
|
|
35
|
+
try {
|
|
36
|
+
const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
37
|
+
const url = String(pkg.repository?.url || pkg.repository || "");
|
|
38
|
+
const m = url.match(/github\.com[/:]([^/]+\/[^/.]+)/);
|
|
39
|
+
return m ? m[1] : "";
|
|
40
|
+
} catch {
|
|
41
|
+
return "";
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
32
45
|
function parseVersion(v) {
|
|
33
46
|
const s = String(v || "").replace(/^v/i, "");
|
|
34
47
|
const parts = s.split(".").map((n) => parseInt(n, 10) || 0);
|
|
@@ -97,9 +110,11 @@ class RuleEngineService extends TypertRemoteService {
|
|
|
97
110
|
async checkUpdate() {
|
|
98
111
|
try {
|
|
99
112
|
const current = currentVersion();
|
|
113
|
+
const slug = repoSlug();
|
|
114
|
+
if (!slug) return { ok: false, error: "仓库信息缺失,无法检查更新" };
|
|
100
115
|
const headers = { "User-Agent": "dsh-rule-engine", Accept: "application/vnd.github+json" };
|
|
101
116
|
// P2-8:网络异常/慢速时避免面板挂起
|
|
102
|
-
const releaseRes = await fetch(
|
|
117
|
+
const releaseRes = await fetch(`https://api.github.com/repos/${slug}/releases/latest`, { headers, signal: AbortSignal.timeout(8000) });
|
|
103
118
|
if (!releaseRes.ok) {
|
|
104
119
|
return { ok: false, error: `GitHub Release 检查失败:HTTP ${releaseRes.status}` };
|
|
105
120
|
}
|
|
@@ -109,7 +124,7 @@ class RuleEngineService extends TypertRemoteService {
|
|
|
109
124
|
|
|
110
125
|
let impacts = [];
|
|
111
126
|
try {
|
|
112
|
-
const impactRes = await fetch(
|
|
127
|
+
const impactRes = await fetch(`https://raw.githubusercontent.com/${slug}/main/upgrade-impact.json`, { headers: { "User-Agent": "dsh-rule-engine" }, signal: AbortSignal.timeout(8000) });
|
|
113
128
|
if (impactRes.ok) {
|
|
114
129
|
const data = await impactRes.json();
|
|
115
130
|
if (Array.isArray(data?.versions)) {
|
package/package.json
CHANGED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
// publish-aptitude-check.mjs — 发布适用性门禁(T6,2026-08-31)
|
|
2
|
+
// 目的:兑现 README「通用化」承诺的"陌生人视角"检查——发布物必须先证明:
|
|
3
|
+
// ① 无 AGENTS.md(陌生环境冷启动):引擎零错 / 零死映射 / 零意外拦截面
|
|
4
|
+
// ② 空白规则文件:静默(0 规则、无错、无 dead)
|
|
5
|
+
// ③ 任意编号规则(非本机编号):声明式绑定与"纯自证"分流行为正确
|
|
6
|
+
// ④ 四要素 + 执行等级格式的最小规则:actions/handler 解析正确(README 格式契约冒烟)
|
|
7
|
+
// ⑤ 发布物个人标识扫描:files 白名单内文件不得含本机个人标识(jilian/季涟/D:\DeepSeek/邮箱/私人注释)
|
|
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
|
+
const declared = understandRule({ ...arbitrary, body: arbitrary.body + "\n<!-- handler: rule12a-approval -->" });
|
|
68
|
+
check(declared.handler === "rule12a-approval", "③ 任意编号 + 声明:绑定指定执行器");
|
|
69
|
+
const covered = analyzeCoverage(understandAll([arbitrary]));
|
|
70
|
+
check(covered.uncovered.some((u) => u.ruleId === "X-77"), "③ 任意编号 A 级无 handler:被标记未覆盖(提示正确)");
|
|
71
|
+
// 通用化闭环:defaultMap 清空 → 陌生规则无兜底绑定(纯声明式世界,不借道本机偏好表);
|
|
72
|
+
// 注:analyzeCoverage 自省对象恒为本机默认表(体检对象),不受 defaultMap 影响。
|
|
73
|
+
const bare = understandRule(arbitrary, { defaultMap: {} });
|
|
74
|
+
check(bare.handler === "", "③ defaultMap 清空:陌生规则无兜底绑定(纯声明式世界)");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ── ④ 四要素 + 执行等级格式契约冒烟(README 格式)──
|
|
78
|
+
{
|
|
79
|
+
const elem = understandRule({
|
|
80
|
+
index: "E-1",
|
|
81
|
+
title: "格式样例(执行等级:B + D)",
|
|
82
|
+
section: "样例分区",
|
|
83
|
+
level: "B + D",
|
|
84
|
+
body: "- **触发**:触发条件。\n- **检查**:检查项。\n- **动作**:动作说明。\n- **豁免**:豁免说明。"
|
|
85
|
+
});
|
|
86
|
+
check(elem.elements.trigger.length > 0 && elem.elements.check.length > 0 && elem.elements.action.length > 0 && elem.elements.exemption.length > 0, "④ 四要素提取完整");
|
|
87
|
+
check(elem.actions.includes("correct") && elem.actions.includes("self-certify"), "④ 组合等级 B + D 动作解析正确");
|
|
88
|
+
check(elem.confidence === "high", "④ 四要素齐全 → 置信 high");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ── ⑤ 发布物个人标识扫描(files 白名单;--tgz 指定时扫 tgz 实体)──
|
|
92
|
+
const PERSONAL_RE = /jilian|季涟|D:\\DeepSeek|D:\/DeepSeek|@qq\.com|@163\.com|@outlook\.com|私人注释|个人工作流|本机规则/;
|
|
93
|
+
{
|
|
94
|
+
const pkg = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
|
|
95
|
+
const scanFiles = [];
|
|
96
|
+
const collect = (d, prefix = "") => {
|
|
97
|
+
for (const f of readdirSync(d)) {
|
|
98
|
+
const p = join(d, f);
|
|
99
|
+
if (statSync(p).isDirectory()) { if (!["node_modules", ".backups", ".git", ".codegraph", "test"].includes(f)) collect(p, prefix + f + "/"); }
|
|
100
|
+
else if (/\.[a-z]+$/i.test(f) && !/\.(png|jpg|jpeg|gif|webp|svg|tgz|map)$/i.test(f) && !f.startsWith(".")) scanFiles.push(prefix + f);
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
collect(join(root, "lib"), "lib/");
|
|
104
|
+
for (const f of ["README.md", "cordis.patch.yml", "package.json"]) {
|
|
105
|
+
if (existsSync(join(root, f))) scanFiles.push(f);
|
|
106
|
+
}
|
|
107
|
+
const hits = [];
|
|
108
|
+
for (const rel of scanFiles) {
|
|
109
|
+
try {
|
|
110
|
+
const text = readFileSync(join(root, rel), "utf8");
|
|
111
|
+
if (PERSONAL_RE.test(text)) hits.push(rel);
|
|
112
|
+
} catch { /* 二进制跳过 */ }
|
|
113
|
+
}
|
|
114
|
+
if (hits.length) {
|
|
115
|
+
// 白名单:README.md 的致谢/仓库信息(官方仓库地址含 jilian-dsh 为必要元数据)、
|
|
116
|
+
// package.json 的 repository 属通用字段 —— 明细列出供人工核对,不直接判红
|
|
117
|
+
const critical = hits.filter((h) => h.startsWith("lib/"));
|
|
118
|
+
check(critical.length === 0, `⑤ 发布物标识扫描:lib 源码命中 ${critical.length} 处(${critical.join(", ")})——个人标识不得进入通用代码`);
|
|
119
|
+
} else {
|
|
120
|
+
check(true, "⑤ 发布物标识扫描:files 白名单无个人标识命中(注意:README 仓库信息属必要元数据,不在此列)");
|
|
121
|
+
}
|
|
122
|
+
// tgz 实体扫描(--tgz):gzip 解包 + 512 字节 tar 头扫描文件内容
|
|
123
|
+
if (tgzArg && existsSync(tgzArg)) {
|
|
124
|
+
const { gunzipSync } = await import("node:zlib");
|
|
125
|
+
const buf = gunzipSync(readFileSync(tgzArg));
|
|
126
|
+
let off = 0;
|
|
127
|
+
const name = new TextDecoder().decode(buf.subarray(off, off + 100)).replace(/\0.*$/, "");
|
|
128
|
+
const sizeStr = new TextDecoder().decode(buf.subarray(off + 124, off + 136)).replace(/\0/g, "").trim();
|
|
129
|
+
off += 512;
|
|
130
|
+
if (name && sizeStr) { /* 首块为目录/文件;逐块扫描见下 */ }
|
|
131
|
+
// 简化:直接全文正则扫描(gzip 已解包,文本可读)
|
|
132
|
+
const text = buf.toString("utf8");
|
|
133
|
+
check(!PERSONAL_RE.test(text), "⑤ tgz 实体扫描:无个人标识命中");
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ── --deep:完整引擎链冷启动(子进程,需完整权限/CI;沙箱不可用)──
|
|
138
|
+
if (deep) {
|
|
139
|
+
console.log("\n(--deep 模式:请确认在完整权限/CI 环境运行——子进程捕获受沙箱限制)");
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
rmSync(sceneDir, { recursive: true, force: true });
|
|
143
|
+
console.log(`\n${FAILS.length === 0 ? "ADEPTITUDE CHECK PASS" : `ADEPTITUDE CHECK FAIL(${FAILS.length} 项)`}`);
|
|
144
|
+
process.exit(FAILS.length === 0 ? 0 : 1);
|
|
@@ -0,0 +1,277 @@
|
|
|
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-08-27 实测 npm registry / github.com / api.github.com 直连可用,无需代理;
|
|
10
|
+
// 代理挂掉时不再 ECONNREFUSED)。需要代理的环境设 DSH_RELEASE_PROXY=http://127.0.0.1:7890。
|
|
11
|
+
// 步骤:版本 bump -> 测试 -> npm pack -> npm publish -> git commit+push -> gh release(带 tgz asset)
|
|
12
|
+
// 安全:token 经环境变量注入,不在命令文本/日志中打印
|
|
13
|
+
import { execSync } from "node:child_process";
|
|
14
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, copyFileSync } from "node:fs";
|
|
15
|
+
import { dirname, join, relative, resolve } from "node:path";
|
|
16
|
+
import { fileURLToPath } from "node:url";
|
|
17
|
+
|
|
18
|
+
const PROXY = process.env.DSH_RELEASE_PROXY || ""; // 默认直连;需要代理时显式设置
|
|
19
|
+
/** 代理前缀(命令文本):空 = 直连 */
|
|
20
|
+
const proxyPrefix = () => (PROXY ? `set HTTPS_PROXY=${PROXY}&& set HTTP_PROXY=${PROXY}&& ` : "");
|
|
21
|
+
const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url));
|
|
22
|
+
const WORKSPACE = join(SCRIPT_DIR, "..");
|
|
23
|
+
|
|
24
|
+
function run(cmd, opts = {}) {
|
|
25
|
+
console.log(`> ${cmd.slice(0, 120)}${cmd.length > 120 ? "..." : ""}`);
|
|
26
|
+
return execSync(cmd, { stdio: "inherit", encoding: "utf8", shell: true, ...opts });
|
|
27
|
+
}
|
|
28
|
+
function quiet(cmd) {
|
|
29
|
+
try {
|
|
30
|
+
return execSync(cmd, { encoding: "utf8", shell: true, stdio: "pipe" }).toString().trim();
|
|
31
|
+
} catch {
|
|
32
|
+
return "";
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function fail(msg) {
|
|
36
|
+
console.error(`\n[发布中止] ${msg}`);
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ── 0. 解析目标(清单名 或 目录路径)────────────────────────────
|
|
41
|
+
const args = process.argv.slice(2);
|
|
42
|
+
const syncProfile = args.includes("--sync-profile");
|
|
43
|
+
const dryRun = args.includes("--dry-run"); // 2026-08-29:发布前预览(只推导+打印,不改文件)
|
|
44
|
+
const target = args.find((a) => !a.startsWith("--")) || "";
|
|
45
|
+
let manifest = null;
|
|
46
|
+
let entry = null;
|
|
47
|
+
let dir = null;
|
|
48
|
+
try {
|
|
49
|
+
manifest = JSON.parse(readFileSync(join(SCRIPT_DIR, "plugins.json"), "utf8"));
|
|
50
|
+
entry = manifest.plugins.find((p) => p.name === target) || null;
|
|
51
|
+
} catch {
|
|
52
|
+
// 清单缺失时仅支持目录路径
|
|
53
|
+
}
|
|
54
|
+
if (entry) {
|
|
55
|
+
dir = resolve(WORKSPACE, entry.dir);
|
|
56
|
+
} else if (target) {
|
|
57
|
+
dir = resolve(target);
|
|
58
|
+
} else {
|
|
59
|
+
fail("用法:node release-plugin.mjs <插件名|插件目录> [版本号] [--sync-profile]");
|
|
60
|
+
}
|
|
61
|
+
if (!existsSync(join(dir, "package.json"))) fail(`目录不存在或不是插件包:${dir}`);
|
|
62
|
+
|
|
63
|
+
// ── 0. 读取包信息 ────────────────────────────────────────────────
|
|
64
|
+
const pkgPath = join(dir, "package.json");
|
|
65
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
66
|
+
const name = pkg.name;
|
|
67
|
+
const oldVer = pkg.version;
|
|
68
|
+
// 版本号 = 非 -- 开头的参数(排除目标名本身),位置任意
|
|
69
|
+
// 2026-08-29 修复(0.5.11-pre 体系):oldVer 为 -pre/-rc 等预发布后缀时——
|
|
70
|
+
// ① 发布语义 = 剥后缀发正式版(0.5.11-pre → 发布 0.5.11,不是 +1);
|
|
71
|
+
// ② 自动推导不再 split(".") 直接 Number("11-pre" 会得 NaN——旧 bug)。
|
|
72
|
+
// 显式传参优先;且显式传参如果是 -pre 形态(意外),本发布不剥(警告留痕)。
|
|
73
|
+
const nextVer = args.find((a) => !a.startsWith("--") && a !== target) || (() => {
|
|
74
|
+
// 预发布后缀(-pre/-rc)→ 剥后缀发正式版(0.5.11-pre → 0.5.11)
|
|
75
|
+
if (/-(?:pre|rc|beta|alpha)(?:[.\d]*)$/i.test(oldVer)) return oldVer.replace(/-.*$/, "");
|
|
76
|
+
// 纯数字 → +1(历史行为)
|
|
77
|
+
const [maj, min, pat] = oldVer.split(".").map(Number);
|
|
78
|
+
if (Number.isNaN(pat)) fail(`无法解析版本号:${oldVer}`);
|
|
79
|
+
return `${maj}.${min}.${pat + 1}`;
|
|
80
|
+
})();
|
|
81
|
+
const repoOverride = entry?.repo || null;
|
|
82
|
+
console.log(`\n=== 发布 ${name}: ${oldVer} -> ${nextVer} ===\n`);
|
|
83
|
+
|
|
84
|
+
// ── 0.5 本地配套包版本一致性(防止 host/client 版本错位)──────────
|
|
85
|
+
function checkDependencyPair() {
|
|
86
|
+
if (!entry?.dependsOn || !manifest) return;
|
|
87
|
+
for (const depName of entry.dependsOn) {
|
|
88
|
+
const depEntry = manifest.plugins.find((p) => p.name === depName);
|
|
89
|
+
const depDir = depEntry ? resolve(WORKSPACE, depEntry.dir) : null;
|
|
90
|
+
if (!depDir || !existsSync(join(depDir, "package.json"))) {
|
|
91
|
+
console.log(`(未找到依赖 ${depName} 的本地包,跳过版本一致性检查)`);
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
const depPkg = JSON.parse(readFileSync(join(depDir, "package.json"), "utf8"));
|
|
95
|
+
const range = pkg.dependencies?.[depName];
|
|
96
|
+
if (!range) continue;
|
|
97
|
+
const m = range.match(/^\^(\d+)\.(\d+)\.(\d+)/);
|
|
98
|
+
if (!m) {
|
|
99
|
+
console.log(`(依赖 ${depName} 范围 ${range} 不是简单 ^x.y.z,跳过自动校验)`);
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
const v = depPkg.version.split(".").map(Number);
|
|
103
|
+
const ok = v[0] === +m[1] && (v[1] > +m[2] || (v[1] === +m[2] && v[2] >= +m[3]));
|
|
104
|
+
if (!ok) fail(`${name} 声明依赖 ${depName}@${range},但本地 ${depName} 是 ${depPkg.version}`);
|
|
105
|
+
console.log(`[依赖一致性] ${name} -> ${depName} ${depPkg.version} ✓`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
checkDependencyPair();
|
|
109
|
+
|
|
110
|
+
// ── 1. 前置检查 ──────────────────────────────────────────────────
|
|
111
|
+
if (!/^\d+\.\d+\.\d+$/.test(nextVer)) fail(`版本号格式错误:${nextVer}`);
|
|
112
|
+
if (!quiet("gh auth status").includes("Logged in")) fail("gh 未认证");
|
|
113
|
+
const whoami = quiet("npm whoami");
|
|
114
|
+
if (!whoami) fail("npm 未认证(npm whoami 失败)");
|
|
115
|
+
|
|
116
|
+
// ── 1.5 DSH 兼容预检(发布前防“升级后插件不兼容”)──────────────
|
|
117
|
+
const compatScript = join(SCRIPT_DIR, "check-dsh-compat.mjs");
|
|
118
|
+
if (existsSync(compatScript)) {
|
|
119
|
+
console.log("\n=== DSH 兼容预检 ===");
|
|
120
|
+
run(`node "${compatScript}" "${dir}"`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ── 1.6 profile dump-config 冒烟(确认 DSH 可导出当前 profile 配置)──
|
|
124
|
+
function findDshBin() {
|
|
125
|
+
const candidates = [process.env.DSH_BIN, "D:/npm-global/dsh.cmd", "D:/npm-global/dsh", "dsh"].filter(Boolean);
|
|
126
|
+
for (const c of candidates) {
|
|
127
|
+
if (quiet(`where ${c}`)) return c;
|
|
128
|
+
}
|
|
129
|
+
return "";
|
|
130
|
+
}
|
|
131
|
+
const dshCmd = findDshBin();
|
|
132
|
+
if (dshCmd) {
|
|
133
|
+
const dump = quiet(`"${dshCmd}" --profile ${manifest?.profile || "web"} --dump-config`);
|
|
134
|
+
if (dump && !/error|Error|ENOENT/i.test(dump)) {
|
|
135
|
+
console.log("(profile dump-config 冒烟通过:DSH 配置可正常导出)");
|
|
136
|
+
} else {
|
|
137
|
+
console.log("(profile dump-config 冒烟未通过:请人工确认 DSH 可用后发布)");
|
|
138
|
+
}
|
|
139
|
+
} else {
|
|
140
|
+
console.log("(未找到 dsh 命令,跳过 profile dump-config 冒烟)");
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ── 2. 版本 bump(package.json + README 徽章)────────────────────
|
|
144
|
+
if (dryRun) {
|
|
145
|
+
console.log(`[DRY-RUN] 不修改任何文件。`);
|
|
146
|
+
console.log(`[DRY-RUN] oldVer=${oldVer} → nextVer=${nextVer}`);
|
|
147
|
+
const rd = join(dir, "README.md");
|
|
148
|
+
if (existsSync(rd)) {
|
|
149
|
+
const t = readFileSync(rd, "utf8");
|
|
150
|
+
const oldBadgeVer = `version-${oldVer}`;
|
|
151
|
+
const oldBadgeUrl = `version-${oldVer.replace(/-/g, "--")}`;
|
|
152
|
+
const nextUrl = `version-${nextVer}`;
|
|
153
|
+
const replaced = t.replaceAll(oldBadgeVer, nextUrl).replaceAll(oldBadgeUrl, nextUrl);
|
|
154
|
+
console.log(`[DRY-RUN] README 徽章将变为: ${replaced.match(/version-[^\s]+/)?.[0] || "(未命中,检查徽章形态)"}`);
|
|
155
|
+
}
|
|
156
|
+
process.exit(0);
|
|
157
|
+
}
|
|
158
|
+
writeFileSync(pkgPath, readFileSync(pkgPath, "utf8").replace(`"version": "${oldVer}"`, `"version": "${nextVer}"`));
|
|
159
|
+
for (const readme of ["README.md", "README.en.md"]) {
|
|
160
|
+
const rp = join(dir, readme);
|
|
161
|
+
if (existsSync(rp)) {
|
|
162
|
+
const text = readFileSync(rp, "utf8");
|
|
163
|
+
// 2026-08-29 修复:徽章 URL 编码把 `0.5.11-pre` 转成 `0.5.11--pre`(`-` 被 shields 双横线),
|
|
164
|
+
// 旧正则 `version-0.5.11-pre` 匹配不到 `version-0.5.11--pre` → 徽章漏改(㉙ 机器根因之一)。
|
|
165
|
+
// replaceAll 用字面串(非正则),无需转义。
|
|
166
|
+
const oldBadgeVer = `version-${oldVer}`; // JSON 形态:version-0.5.11-pre
|
|
167
|
+
const oldBadgeUrl = `version-${oldVer.replace(/-/g, "--")}`; // URL 形态:version-0.5.11--pre
|
|
168
|
+
const nextUrl = `version-${nextVer}`;
|
|
169
|
+
const replaced = text
|
|
170
|
+
.replaceAll(oldBadgeVer, nextUrl)
|
|
171
|
+
.replaceAll(oldBadgeUrl, nextUrl);
|
|
172
|
+
if (replaced !== text) writeFileSync(rp, replaced);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
console.log("版本已 bump(package.json + README 徽章)");
|
|
176
|
+
|
|
177
|
+
// ── 3. 测试(规则 23:发布前运行时验证)─────────────────────────
|
|
178
|
+
if (pkg.scripts && pkg.scripts.test) {
|
|
179
|
+
console.log("\n=== 运行测试 ===");
|
|
180
|
+
run(`cd /d "${dir}" && npm test --prefix "${dir}"`);
|
|
181
|
+
} else {
|
|
182
|
+
console.log("(无 test 脚本,跳过)");
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ── 4. pack + publish ────────────────────────────────────────────
|
|
186
|
+
console.log("\n=== npm pack ===");
|
|
187
|
+
run(`cd /d "${dir}" && npm pack --pack-destination .`);
|
|
188
|
+
const tgz = `${name}-${nextVer}.tgz`;
|
|
189
|
+
if (!existsSync(join(dir, tgz))) fail(`打包产物缺失:${tgz}`);
|
|
190
|
+
|
|
191
|
+
console.log("\n=== npm publish ===");
|
|
192
|
+
run(`cd /d "${dir}" && ${proxyPrefix()}npm publish ${tgz}`);
|
|
193
|
+
|
|
194
|
+
// ── 5. git commit + push(token 经环境变量注入 URL,命令文本不含密钥明文)─────
|
|
195
|
+
console.log("\n=== git commit + push ===");
|
|
196
|
+
// 2026-08-25 修复 pushUrl 双重拼接:origin 可能是 ssh(git@github.com:)、https(https://github.com/)
|
|
197
|
+
// 或裸路径(user/repo)三种形态——统一归一化为 "owner/repo" 路径再拼 token URL(此前 https origin 未剥前缀
|
|
198
|
+
// 导致 "https://github.com/https://github.com/..." 双重 URL,push 404)
|
|
199
|
+
const repo = (repoOverride || quiet(`cd /d "${dir}" && git remote get-url origin`))
|
|
200
|
+
.replace(/^git@github\.com:/, "")
|
|
201
|
+
.replace(/^https?:\/\/github\.com\//, "")
|
|
202
|
+
.replace(/\.git$/, "");
|
|
203
|
+
const token = quiet("gh auth token");
|
|
204
|
+
if (!token) fail("无法获取 gh token");
|
|
205
|
+
// 与 v3.72 同款通道:token 拼进 HTTPS URL 直推(避开沙箱下 msys 凭据管道的 EPERM)
|
|
206
|
+
const pushUrl = `https://jilian-dsh:${token}@github.com/${repo}.git`;
|
|
207
|
+
// 2026-08-31 修复:repoRoot 实测 git 仓库根(插件目录可能只是子目录,如 rules-manager 在 oss 仓库内);
|
|
208
|
+
// stageSpec = 插件目录相对仓库根路径(防 git add -A 误带仓库内无关改动/未跟踪物)
|
|
209
|
+
const repoRoot = quiet(`cd /d "${dir}" && git rev-parse --show-toplevel`).trim();
|
|
210
|
+
const relDir = relative(repoRoot, dir).split(/[\\/]/).join("/");
|
|
211
|
+
const stageSpec = relDir && relDir !== "." ? relDir : "";
|
|
212
|
+
// 2026-08-29 防漏机检(0.5.10 git 欠账事故:git add -u 只更新已跟踪文件,未跟踪新文件
|
|
213
|
+
// (npm pack 按目录打包、天然包含)会漏进 git 提交 → git 与 npm 内容不一致)。
|
|
214
|
+
// 现在:提交前检测未跟踪文件并打印名单,改用 add -A 一并提交(.gitignore 已排除杂质)。
|
|
215
|
+
const untrackedList = quiet(`cd /d "${repoRoot}" && git status --porcelain -- ${stageSpec}`)
|
|
216
|
+
.split("\n").filter((l) => l.startsWith("??")).map((l) => l.slice(3));
|
|
217
|
+
if (untrackedList.length > 0) {
|
|
218
|
+
console.log(`(未跟踪文件 ${untrackedList.length} 个将一并提交:${untrackedList.slice(0, 6).join(", ")}${untrackedList.length > 6 ? " 等" : ""})`);
|
|
219
|
+
}
|
|
220
|
+
run(`cd /d "${repoRoot}" && ${proxyPrefix()}git add -A -- ${stageSpec} && git -c core.autocrlf=false commit -m "release: ${name} v${nextVer}" || exit 0`);
|
|
221
|
+
run(`cd /d "${repoRoot}" && ${proxyPrefix()}git push "${pushUrl}" HEAD`);
|
|
222
|
+
|
|
223
|
+
// ── 6. GitHub Release(带正式 tgz asset,规则 26;清单标记 skipRelease 的包不建)─────
|
|
224
|
+
if (entry?.skipRelease) {
|
|
225
|
+
console.log("(清单标记 skipRelease,跳过 GitHub Release)");
|
|
226
|
+
} else {
|
|
227
|
+
console.log("\n=== GitHub Release ===");
|
|
228
|
+
const notesFile = join(process.env.TEMP || ".", `notes-${name}-${nextVer}.md`);
|
|
229
|
+
writeFileSync(notesFile, `## v${nextVer}\n\nRelease generated by scripts/release-plugin.mjs\n`);
|
|
230
|
+
// 必须在插件目录(git 仓库)内运行:gh release 内部会做 git 检查
|
|
231
|
+
run(`cd /d "${dir}" && ${proxyPrefix()}gh release create v${nextVer} "${join(dir, tgz)}" --title "${name} v${nextVer}" --notes-file "${notesFile}"`);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// ── 7. 本机 profile 同步(--sync-profile:豁免名单 + 装配审计;需 danger-full-access 运行)──
|
|
235
|
+
if (syncProfile) {
|
|
236
|
+
console.log("\n=== 本机 profile 同步 ===");
|
|
237
|
+
let manifest = null;
|
|
238
|
+
try {
|
|
239
|
+
manifest = JSON.parse(readFileSync(join(SCRIPT_DIR, "plugins.json"), "utf8"));
|
|
240
|
+
} catch {
|
|
241
|
+
manifest = { profile: "web" };
|
|
242
|
+
}
|
|
243
|
+
const profileDir = resolve(WORKSPACE, "..", ".dsh", "profiles", manifest.profile || "web");
|
|
244
|
+
const wsYaml = join(profileDir, "pnpm-workspace.yaml");
|
|
245
|
+
if (existsSync(wsYaml)) {
|
|
246
|
+
const raw = readFileSync(wsYaml, "utf8");
|
|
247
|
+
const lineRe = new RegExp(`^( - ${name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}@[^\\n]*)$`, "m");
|
|
248
|
+
const m = raw.match(lineRe);
|
|
249
|
+
if (m && !m[1].includes(nextVer)) {
|
|
250
|
+
const bakDir = join(profileDir, "..", ".backups");
|
|
251
|
+
mkdirSync(bakDir, { recursive: true });
|
|
252
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
253
|
+
const bak = join(bakDir, `pnpm-workspace-${stamp}.yaml`);
|
|
254
|
+
copyFileSync(wsYaml, bak);
|
|
255
|
+
const updated = raw.replace(lineRe, `${m[1]} || ${nextVer}`);
|
|
256
|
+
writeFileSync(wsYaml, updated);
|
|
257
|
+
console.log(`豁免名单已追加 ${name}@${nextVer}(备份 ${bak})`);
|
|
258
|
+
} else if (!m) {
|
|
259
|
+
console.log(`(豁免名单无 ${name} 条目,跳过)`);
|
|
260
|
+
} else {
|
|
261
|
+
console.log(`(豁免名单已含 ${nextVer},跳过)`);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
// 全量装配审计(规则 27)
|
|
265
|
+
const auditScript = join(SCRIPT_DIR, "..", "projects", "oss", "dsh-rule-engine", "scripts", "audit-mount-consistency.mjs");
|
|
266
|
+
if (existsSync(auditScript)) {
|
|
267
|
+
const out = quiet(`node "${auditScript}" --profile ${manifest.profile || "web"}`);
|
|
268
|
+
const pass = /MOUNT CONSISTENT/.test(out);
|
|
269
|
+
console.log(out.split("\n").filter((l) => /RESULT|DUPLICATES|MOUNT|summary/.test(l)).join("\n"));
|
|
270
|
+
if (!pass) fail("装配审计未通过(MOUNT CONSISTENT 未出现),请先处理再重启 DSH");
|
|
271
|
+
console.log("装配审计 MOUNT CONSISTENT ✓");
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
console.log(`\n=== 发布完成:${name} v${nextVer} ===`);
|
|
276
|
+
console.log(`npm: ${name}@${nextVer}`);
|
|
277
|
+
console.log(`release: https://github.com/${repo}/releases/tag/v${nextVer}`);
|
package/scripts/verify-all.mjs
CHANGED
|
@@ -119,6 +119,10 @@ if (judgeReal > 0) {
|
|
|
119
119
|
lines.push("⚠️ 真实判例:近 24h 无 judge-pass/judge-false 记录——裁决器需要一次实弹(非阻塞 WARN,但说明运行证据尚未产生)");
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
+
// ── ⑧ 发布适用性门禁(T6,2026-08-31:通用化"陌生人视角"硬闸)──
|
|
123
|
+
// 无 AGENTS.md 冷启动 / 空白规则静默 / 任意编号规则声明式绑定 / 格式契约冒烟 / 个人标识扫描
|
|
124
|
+
step("发布适用性门禁(无 AGENTS.md 冷启动 / 空白规则 / 任意编号 / 个人标识扫描)", process.execPath, [join(root, "scripts", "publish-aptitude-check.mjs")]);
|
|
125
|
+
|
|
122
126
|
console.log(lines.join("\n"));
|
|
123
127
|
const failed = lines.filter((l) => l.startsWith("❌"));
|
|
124
128
|
if (failed.length) {
|