dsh-rule-engine 0.6.0 → 0.6.2
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 +9 -4
- package/lib/core/authorization.js +10 -6
- package/lib/core/guard-core.js +3 -1
- package/lib/core/intent.js +322 -322
- package/lib/core/mount-signature.js +87 -87
- package/lib/core/patterns.js +744 -737
- package/lib/core/state.js +1 -0
- package/lib/core/text-detect.js +19 -0
- package/lib/core/tool-catalog.js +3 -0
- package/lib/index.js +4 -2
- package/lib/service.js +7 -2
- package/package.json +3 -3
- package/scripts/audit-mount-consistency.mjs +5 -5
- package/scripts/lib/pnpm-exempt.mjs +56 -0
- package/scripts/release-plugin.mjs +45 -1
- package/scripts/verify-all.mjs +77 -1
package/lib/core/state.js
CHANGED
|
@@ -247,6 +247,7 @@ export function freshTurn() {
|
|
|
247
247
|
realUserSeen: false, // v0.5.7:本回合是否有真实用户消息(注入轮不检测的依据)
|
|
248
248
|
askSeen: false,
|
|
249
249
|
askRejected: false, // 本回合 ask 已被拒(防连环 ask,弹窗消减)
|
|
250
|
+
askApproved: false, // 2026-09-06 M7 修复:本回合已获 ask 授权答复(M7 approval-gap 豁免信号)
|
|
250
251
|
questionOnly: false,
|
|
251
252
|
intents: null,
|
|
252
253
|
intentState: "lexicon", // lexicon | llm-pending | llm-ready(LLM 意图兜底状态)
|
package/lib/core/text-detect.js
CHANGED
|
@@ -35,6 +35,15 @@ export const CRITICISM_WEAK_RE =
|
|
|
35
35
|
|
|
36
36
|
export const DELIVERY_RE = /(?:已完成|全部(?:[^\s,。;!?]{0,12})完成|已[^\s,。;!?]{0,8}完成|修复完成|落盘完成|验证[^\s,。;!?]{0,6}(?:通过|成功)|全部通过|全部[^\s,。;!?]{0,10}通过|已通过|已修复|搞定)(?:\s*了|!|!|,[^。]*)?(?![^。]*(?:尚未|没有|未|没|还没|未完|待做|待完成))/;
|
|
37
37
|
|
|
38
|
+
// 2026-09-06 0.6.x(tsk_527d222c 第一期):路径缩写 B 级检测——回复含缩写路径(reports\ / ~/.dsh / %USERPROFILE%)
|
|
39
|
+
// 且非完整盘符形态 → 提醒写完整绝对路径(规则 14⑤ 机器化第一层;WEAK 形态:留痕+注入,不拦截)
|
|
40
|
+
const PATH_ABBREV_RE = /(?:^|[\s"'“”((])(?:reports|~\/?\.dsh|%USERPROFILE%)[\\/]/i;
|
|
41
|
+
const ABS_PATH_RE = /[A-Za-z]:[\\/][^\n]*/g;
|
|
42
|
+
export function hasPathAbbrev(text) {
|
|
43
|
+
if (typeof text !== "string") return false;
|
|
44
|
+
return PATH_ABBREV_RE.test(String(text).replace(ABS_PATH_RE, ""));
|
|
45
|
+
}
|
|
46
|
+
|
|
38
47
|
/** 从 assistant message 内容中提取纯文本 */
|
|
39
48
|
export function extractAssistantText(message) {
|
|
40
49
|
if (!message) return "";
|
|
@@ -184,6 +193,16 @@ export function detectViolations({ configs, session, text, reasoningText = "", m
|
|
|
184
193
|
});
|
|
185
194
|
}
|
|
186
195
|
|
|
196
|
+
// 2026-09-06 0.6.x(tsk_527d222c 第一期):路径缩写 B 级(规则 14⑤ 机器化——WEAK,留痕+注入不拦)
|
|
197
|
+
if (hasPathAbbrev(text)) {
|
|
198
|
+
hits.push({
|
|
199
|
+
ruleId: "14",
|
|
200
|
+
title: "汇报规范(完整文件位置路径)",
|
|
201
|
+
kind: "correct",
|
|
202
|
+
reason: "回复含缩写路径(reports\\ / ~/.dsh / %USERPROFILE% 等)——给用户应写完整盘符绝对路径(规则 14⑤)"
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
187
206
|
const sourceCfg = byId.get("5");
|
|
188
207
|
if (sourceCfg && !SOURCE_MARK.test(text)) {
|
|
189
208
|
if (URL_RE.test(text)) {
|
package/lib/core/tool-catalog.js
CHANGED
|
@@ -26,6 +26,9 @@ const ANALYSIS_TOOLS = new Set([
|
|
|
26
26
|
// 子代理交付工具(官方 dsh-tool-subagent-report,2026-08-24 实测发现):仅子代理环境注册,
|
|
27
27
|
// 参数仅 output 文本 → 直接交付父代理,无文件副作用 → analysis(避免子代理交付被 unknown deny 阻断)
|
|
28
28
|
"report",
|
|
29
|
+
// 官方子代理模型发现(@deepseek-ai/dsh-tool-subagent,2026-09-06 工具目录漂移补录):只读枚举
|
|
30
|
+
// 可用子代理模型(模型发现/所选路由校验),无持久副作用 → analysis
|
|
31
|
+
"list_subagent_models",
|
|
29
32
|
// 官方 0.5.9 补全(tool-catalog 镜像 2026-08-27 交叉)
|
|
30
33
|
"lsp",
|
|
31
34
|
"session_event_read", "session_event_search", "session_event_trace", "session_search", "session_trace",
|
package/lib/index.js
CHANGED
|
@@ -563,7 +563,7 @@ export function handleSessionEvent(ctx, session, event) {
|
|
|
563
563
|
if (!text) return;
|
|
564
564
|
// 引擎自身注入的 [规则引擎] 提示不是用户消息,跳过——否则其中的“请直接执行”会被记录成授权(自我续授权漏洞)
|
|
565
565
|
if (isEngineInjectedMessage(text)) return;
|
|
566
|
-
// 模板兜底(对 kind=user 同样生效,防"标 user 的注入"——
|
|
566
|
+
// 模板兜底(对 kind=user 同样生效,防"标 user 的注入"——example-injector createUserMessage 同类风险):
|
|
567
567
|
// runtime context 快照 / 子代理完成通知 / vision-router 挂载提醒等已知注入模板 → 整体跳过
|
|
568
568
|
if (isKnownSystemInjection(text)) {
|
|
569
569
|
audit({
|
|
@@ -971,6 +971,7 @@ export function handleSessionEvent(ctx, session, event) {
|
|
|
971
971
|
const result = d.result ?? d.value ?? d;
|
|
972
972
|
if (askResultApproved(result)) {
|
|
973
973
|
s.turn.realUserSeen = true; // v0.5.7:ask 弹窗答复也算"真实用户在场"
|
|
974
|
+
s.turn.askApproved = true; // 2026-09-06 M7 修复:ask 授权答复 = 本回合已授权(M7 approval-gap 豁免)
|
|
974
975
|
const qText = askQuestionText(pending.questions);
|
|
975
976
|
const selectedText = askResultSelectedText(result);
|
|
976
977
|
// 授权范围只取“问题核心 + 用户实际选择”,不再把全部选项描述纳入;
|
|
@@ -1642,7 +1643,8 @@ export function apply(ctx) {
|
|
|
1642
1643
|
// 边界裁决(v1.3 关键裁决 4):正则保持无尾部 \b(保守匹配,xxx.mjs.bak 等变体同样命中)。
|
|
1643
1644
|
if ((exec?.name === "pwsh" || exec?.name === "bash") && entryScript && new RegExp(`\\b${entryScript.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`).test(cmd)) {
|
|
1644
1645
|
const s7 = getSessionState(state, sessionIdOfExec(exec));
|
|
1645
|
-
|
|
1646
|
+
// 2026-09-06 M7 修复:ask 授权答复(turn.askApproved)→ 豁免 approval-gap(12A 授权链已由 ask 建立)
|
|
1647
|
+
if (needsApprovalReminder(s7.turn.userText || "", { askApproved: !!s7.turn.askApproved })) {
|
|
1646
1648
|
audit({
|
|
1647
1649
|
kind: "approval-gap",
|
|
1648
1650
|
rule: "__approval-gap",
|
package/lib/service.js
CHANGED
|
@@ -64,9 +64,14 @@ class RuleEngineService extends TypertRemoteService {
|
|
|
64
64
|
|
|
65
65
|
constructor(ctx) {
|
|
66
66
|
super(ctx, "ruleEngine");
|
|
67
|
+
// 0.1.2 适配(2026-09-07,实markers 验证):typert-protocol 0.1.2 Remote() 三分支——
|
|
68
|
+
// ① 一参字符串=exportName 工厂;② typeof 一参==='object'(含 null!)=流选项校验
|
|
69
|
+
// (仅容 {mode:'stream'},其余 TypeError——Remote(null,…) 在 0.1.2 因 typeof null===
|
|
70
|
+
// 'object' 落此分支崩溃,即本次升级事故 engine 侧根因;0.1.1 的 null 直落③分支);
|
|
71
|
+
// ③ 一参 undefined + 二参 context=addMarkerInitializer(context,{kind:'direct'})。
|
|
72
|
+
// 手动标记形态=Remote(undefined, fakeContext),context 含 {name,private,static,addInitializer}。
|
|
67
73
|
for (const name of REMOTE_METHODS) {
|
|
68
|
-
Remote(
|
|
69
|
-
kind: "method",
|
|
74
|
+
Remote(undefined, {
|
|
70
75
|
name,
|
|
71
76
|
private: false,
|
|
72
77
|
static: false,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-rule-engine",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.2",
|
|
4
4
|
"description": "DSH 规则执行引擎 v3:容器解析 AGENTS.md + 理解器 + 匹配机 + 执行框架",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -49,8 +49,8 @@
|
|
|
49
49
|
"check:meta": "node scripts/readme-version-check.mjs && node scripts/local-residue-scan.mjs"
|
|
50
50
|
},
|
|
51
51
|
"peerDependencies": {
|
|
52
|
-
"@deepseek-ai/dsh-home-paths": ">=0.1.0-rc.3 <0.2.0 || >=0.1.1-rc.0 <0.2.0",
|
|
53
|
-
"@deepseek-ai/dsh-typert-protocol": ">=0.1.0-rc.3 <0.2.0 || >=0.1.1-rc.0 <0.2.0"
|
|
52
|
+
"@deepseek-ai/dsh-home-paths": ">=0.1.0-rc.3 <0.2.0 || >=0.1.1-rc.0 <0.2.0 || >=0.1.2-rc.0 <0.2.0",
|
|
53
|
+
"@deepseek-ai/dsh-typert-protocol": ">=0.1.0-rc.3 <0.2.0 || >=0.1.1-rc.0 <0.2.0 || >=0.1.2-rc.0 <0.2.0"
|
|
54
54
|
},
|
|
55
55
|
"dsh": {
|
|
56
56
|
"bundle": {
|
|
@@ -153,23 +153,23 @@ for (const [name, spec] of Object.entries(deps)) {
|
|
|
153
153
|
}
|
|
154
154
|
}
|
|
155
155
|
|
|
156
|
-
// 3.5 runtime injection registry (
|
|
156
|
+
// 3.5 runtime injection registry (example-injector) — also produces loader entries
|
|
157
157
|
console.log('\n=== 3.5 runtime injection registry ===')
|
|
158
|
-
const superInjectorRegistry = join(DSH_HOME, '
|
|
158
|
+
const superInjectorRegistry = join(DSH_HOME, 'example-injector', 'registry.json')
|
|
159
159
|
try {
|
|
160
160
|
if (existsSync(superInjectorRegistry)) {
|
|
161
161
|
const reg = JSON.parse(readFileSync(superInjectorRegistry, 'utf8'))
|
|
162
162
|
if (!Array.isArray(reg)) throw new Error('registry.json is not an array')
|
|
163
163
|
const names = reg.map((e) => e && e.name).filter(Boolean)
|
|
164
164
|
for (const name of names) {
|
|
165
|
-
sources.push({ id: name, via: 'runtime injection (
|
|
165
|
+
sources.push({ id: name, via: 'runtime injection (example-injector registry)', file: superInjectorRegistry })
|
|
166
166
|
}
|
|
167
167
|
console.log(' runtime injected:', names.join(', ') || '(none)')
|
|
168
168
|
} else {
|
|
169
|
-
console.log(' (no
|
|
169
|
+
console.log(' (no example-injector registry found; if dev_inject_plugin has been used, run dev_injected_list to confirm)')
|
|
170
170
|
}
|
|
171
171
|
} catch (e) {
|
|
172
|
-
console.log(' [WARN] failed to read
|
|
172
|
+
console.log(' [WARN] failed to read example-injector registry:', e instanceof Error ? e.message : String(e))
|
|
173
173
|
}
|
|
174
174
|
|
|
175
175
|
// 4. duplicate detection across ALL sources
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// pnpm-exempt.mjs — pnpm-workspace minimumReleaseAgeExclude 豁免判定(verify-all ⑬ 与 release-plugin 预插共享单源)
|
|
2
|
+
// 约定(exempt 语义):行含包名(`pkg@`)且版本段任一匹配——yaml 支持 `- pkg@1.5.3 || 1.5.4` 形态(段可带/不带包名前缀)
|
|
3
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
4
|
+
|
|
5
|
+
export function exemptPkg(wsYaml, pkg, ver) {
|
|
6
|
+
if (typeof wsYaml !== "string" || wsYaml.length === 0) return false;
|
|
7
|
+
return wsYaml.split(/\r?\n/).some((l) => l.includes(`${pkg}@`) && l.split(/\s*\|\|\s*/).some((tok) => {
|
|
8
|
+
const t = tok.trim().replace(/^-\s*/, "");
|
|
9
|
+
return t === ver || t === `${pkg}@${ver}`;
|
|
10
|
+
}));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** 追加豁免行(已存在=幂等跳过;返回 {yaml, added}) */
|
|
14
|
+
export function addExemptLine(wsYaml, pkg, ver) {
|
|
15
|
+
if (exemptPkg(wsYaml, pkg, ver)) return { yaml: wsYaml, added: false };
|
|
16
|
+
const sep = wsYaml.endsWith("\n") ? "" : "\n";
|
|
17
|
+
return { yaml: `${wsYaml}${sep} - ${pkg}@${ver}\n`, added: true };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** 读 yaml(fail-closed:读取失败抛错,调用方中止发布) */
|
|
21
|
+
export function readYamlOrThrow(path) {
|
|
22
|
+
try {
|
|
23
|
+
const text = readFileSync(path, "utf8");
|
|
24
|
+
if (!text.includes("minimumReleaseAgeExclude")) throw new Error(`yaml 缺 minimumReleaseAgeExclude 块:${path}`);
|
|
25
|
+
return text;
|
|
26
|
+
} catch (e) {
|
|
27
|
+
if (e.code === "ENOENT") throw new Error(`yaml 不存在(fail-closed):${path}`);
|
|
28
|
+
throw e;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* 预插核心(release-plugin bump 后 publish 前调用;单测可注入 dryRun/write/log):
|
|
34
|
+
* 已存在=幂等跳过;yaml 缺失/坏=抛错(fail-closed);dryRun=只打印不写。
|
|
35
|
+
*/
|
|
36
|
+
export function ensureExempt(yamlPath, pkgName, ver, opts = {}) {
|
|
37
|
+
const { dryRun = false, write = (p, c) => writeFileSync(p, c, "utf8"), log = console.log } = opts;
|
|
38
|
+
let yaml;
|
|
39
|
+
try {
|
|
40
|
+
yaml = readYamlOrThrow(yamlPath);
|
|
41
|
+
} catch (e) {
|
|
42
|
+
throw new Error(`豁免预插失败(fail-closed 中止):${e.message}`);
|
|
43
|
+
}
|
|
44
|
+
const { yaml: nextYaml, added } = addExemptLine(yaml, pkgName, ver);
|
|
45
|
+
if (dryRun) {
|
|
46
|
+
log(`[DRY-RUN] 豁免预插:${added ? `将追加 ${pkgName}@${ver}` : `已存在(幂等跳过)`}`);
|
|
47
|
+
return { added, dryRun: true };
|
|
48
|
+
}
|
|
49
|
+
if (added) {
|
|
50
|
+
write(yamlPath, nextYaml);
|
|
51
|
+
log(`豁免预插:+ ${pkgName}@${ver}`);
|
|
52
|
+
} else {
|
|
53
|
+
log(`豁免预插:${pkgName}@${ver} 已存在(幂等跳过)`);
|
|
54
|
+
}
|
|
55
|
+
return { added };
|
|
56
|
+
}
|
|
@@ -12,9 +12,10 @@
|
|
|
12
12
|
// -> git commit+push -> gh release(带 tgz asset)
|
|
13
13
|
// 安全:token 经环境变量注入,不在命令文本/日志中打印
|
|
14
14
|
import { execSync } from "node:child_process";
|
|
15
|
-
import { existsSync, readFileSync, writeFileSync, mkdirSync, copyFileSync, statSync } from "node:fs";
|
|
15
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, copyFileSync, statSync, rmSync } from "node:fs";
|
|
16
16
|
import { dirname, join, relative, resolve } from "node:path";
|
|
17
17
|
import { fileURLToPath } from "node:url";
|
|
18
|
+
import { ensureExempt } from "./lib/pnpm-exempt.mjs";
|
|
18
19
|
|
|
19
20
|
const PROXY = process.env.DSH_RELEASE_PROXY || ""; // 默认直连;需要代理时显式设置
|
|
20
21
|
/** 代理前缀(命令文本):空 = 直连 */
|
|
@@ -216,6 +217,37 @@ console.log("\n=== 发布门禁 B1(README 版本四性)===");
|
|
|
216
217
|
run(`cd /d "${dir}" && node scripts/readme-version-check.mjs`);
|
|
217
218
|
console.log("\n=== 发布门禁 B2(lib/ 本机痕迹扫描)===");
|
|
218
219
|
run(`cd /d "${dir}" && node scripts/local-residue-scan.mjs`);
|
|
220
|
+
// ── 3.6 存在性扫描(泄露预防,2026-09-05):本机增强门禁——REAL_PATHS_SCAN 指向存在性扫描器 →
|
|
221
|
+
// 0 命中才继续;未设置=WARN(本机工具不进包,通用用户无此工具;发布流水线建议设置)──
|
|
222
|
+
const scanReal = process.env.REAL_PATHS_SCAN;
|
|
223
|
+
if (scanReal) {
|
|
224
|
+
console.log("\n=== 存在性扫描(真实路径判据,0 命中红线)===");
|
|
225
|
+
run(`node "${scanReal}" --root "${dir}"`);
|
|
226
|
+
} else {
|
|
227
|
+
console.log("(未设置 REAL_PATHS_SCAN——存在性扫描跳过(本机增强门禁,建议发布前设置))");
|
|
228
|
+
}
|
|
229
|
+
console.log("(发布门禁 B1/B2/存在性 通过)");
|
|
230
|
+
|
|
231
|
+
// ── 3.7 判据库回归(窗口 B 接入,2026-09-06):净化判据(L1 敏感串全集全历史 0 非白名单 + L2 根段未覆盖 0)
|
|
232
|
+
// 本机增强门禁——JUDGE_SCAN 指向本机判定门禁(stage1 判据库扫描器包装);未设置=WARN ──
|
|
233
|
+
const judgeScan = process.env.JUDGE_SCAN;
|
|
234
|
+
if (judgeScan) {
|
|
235
|
+
console.log("\n=== 判据库回归(净化判据,0 非白名单 / 0 未覆盖 红线)===");
|
|
236
|
+
run(`node "${judgeScan}" --root "${dir}"`);
|
|
237
|
+
} else {
|
|
238
|
+
console.log("(未设置 JUDGE_SCAN——判据库回归跳过(本机增强门禁,建议发布前设置))");
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// ── 3.8 豁免预插(E7,2026-09-08 接线;单源 scripts/lib/pnpm-exempt.mjs):bump 后 publish 前,
|
|
242
|
+
// 把 nextVer 自动预插 profiles/web pnpm-workspace.yaml 的 minimumReleaseAgeExclude——⑬ 绝对口径
|
|
243
|
+
// 要求"publish 后 latest ∈ 豁免名单",预插保证该不变式由发布器维护(防手工遗忘 → 发布后 ⑬ 红)。
|
|
244
|
+
// 幂等(已存在跳过);DRY-RUN 只打印;yaml 读取失败抛错中止发布(fail-closed)。──
|
|
245
|
+
{
|
|
246
|
+
const dshHome = process.env.DSH_HOME || join(process.env.USERPROFILE || "", ".dsh");
|
|
247
|
+
const yamlPath = join(dshHome, "profiles", "web", "pnpm-workspace.yaml");
|
|
248
|
+
console.log("\n=== 豁免预插(minimumReleaseAgeExclude ← nextVer)===");
|
|
249
|
+
ensureExempt(yamlPath, name, nextVer, { dryRun: !!process.env.DRY_RUN, log: console.log });
|
|
250
|
+
}
|
|
219
251
|
|
|
220
252
|
// ── 4. pack + publish ────────────────────────────────────────────
|
|
221
253
|
console.log("\n=== npm pack ===");
|
|
@@ -223,6 +255,18 @@ run(`cd /d "${dir}" && npm pack --pack-destination .`);
|
|
|
223
255
|
const tgz = `${name}-${nextVer}.tgz`;
|
|
224
256
|
if (!existsSync(join(dir, tgz))) fail(`打包产物缺失:${tgz}`);
|
|
225
257
|
|
|
258
|
+
// ── 4.5 发布物存在性扫描(泄露预防硬项,2026-09-05):解包 tgz → 判据库直扫(0 命中才 publish)──
|
|
259
|
+
const scanReal2 = process.env.REAL_PATHS_SCAN;
|
|
260
|
+
if (scanReal2) {
|
|
261
|
+
const unpackDir = join(dir, ".pkg-check-" + nextVer);
|
|
262
|
+
if (existsSync(unpackDir)) rmSync(unpackDir, { recursive: true, force: true });
|
|
263
|
+
mkdirSync(unpackDir, { recursive: true });
|
|
264
|
+
run(`tar -xzf "${join(dir, tgz)}" -C "${unpackDir}"`);
|
|
265
|
+
console.log("\n=== 发布物存在性扫描(解包直扫,0 命中红线)===");
|
|
266
|
+
run(`node "${scanReal2}" --root "${join(unpackDir, "package")}"`);
|
|
267
|
+
console.log("(发布物存在性扫描通过)");
|
|
268
|
+
}
|
|
269
|
+
|
|
226
270
|
console.log("\n=== npm publish ===");
|
|
227
271
|
run(`cd /d "${dir}" && ${proxyPrefix()}npm publish ${tgz}`);
|
|
228
272
|
|
package/scripts/verify-all.mjs
CHANGED
|
@@ -12,9 +12,10 @@
|
|
|
12
12
|
// 任一 ❌ → exit 1;⚠️(WARN)不阻塞但必须明示。
|
|
13
13
|
// 注意:子进程输出捕获需完整权限运行(受限模式 EPERM)。
|
|
14
14
|
import { execFileSync } from "node:child_process";
|
|
15
|
-
import { readdirSync, statSync, readFileSync } from "node:fs";
|
|
15
|
+
import { readdirSync, statSync, readFileSync, existsSync } from "node:fs";
|
|
16
16
|
import { dirname, join } from "node:path";
|
|
17
17
|
import { fileURLToPath } from "node:url";
|
|
18
|
+
import { exemptPkg } from "./lib/pnpm-exempt.mjs";
|
|
18
19
|
|
|
19
20
|
const root = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
20
21
|
const lines = [];
|
|
@@ -130,6 +131,81 @@ step("发布门禁 B1(README 版本四性一致性:package.json/徽章/正
|
|
|
130
131
|
// ── ⑩ 发布门禁 B2(阶段 B,2026-09-04:lib/ 本机痕迹扫描,词表唯一源)──
|
|
131
132
|
step("发布门禁 B2(lib/ 本机痕迹扫描——词表唯一源,命中即红)", process.execPath, [join(root, "scripts", "local-residue-scan.mjs")]);
|
|
132
133
|
|
|
134
|
+
// ── ⑪ 存在性扫描(泄露预防,2026-09-05:真实路径判据——REAL_PATHS_SCAN 指向扫描器,0 命中才算过;
|
|
135
|
+
// 未设置=WARN(本机增强门禁,通用环境无此工具)──
|
|
136
|
+
{
|
|
137
|
+
const scanReal = process.env.REAL_PATHS_SCAN;
|
|
138
|
+
if (scanReal) {
|
|
139
|
+
step("存在性扫描(真实路径判据,0 命中红线)", process.execPath, [scanReal, "--root", root]);
|
|
140
|
+
} else {
|
|
141
|
+
lines.push("⚠️ 存在性扫描:未设置 REAL_PATHS_SCAN(本机增强门禁;发布流水线建议设置)");
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ── ⑫ 判据库回归(窗口 B 接入,2026-09-06:净化判据 L1 敏感串全集全历史 0 非白名单 + L2 根段未覆盖 0
|
|
146
|
+
// 红线——JUDGE_SCAN 指向本机判定门禁(stage1 判据库扫描器包装),未设置=WARN(本机增强门禁)──
|
|
147
|
+
{
|
|
148
|
+
const judgeScan = process.env.JUDGE_SCAN;
|
|
149
|
+
if (judgeScan) {
|
|
150
|
+
step("判据库回归(净化判据全历史 0 非白名单 + L2 未覆盖 0 红线)", process.execPath, [judgeScan, "--root", root]);
|
|
151
|
+
} else {
|
|
152
|
+
lines.push("⚠️ 判据库回归:未设置 JUDGE_SCAN(本机增强门禁;发布流水线建议设置)");
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ── ⑬ pnpm 豁免校验(批次四 04 1.1 B1 口径;E-56 修活后为真跑校验:npm 已发布 latest 必须 ∈ minimumReleaseAgeExclude;
|
|
157
|
+
// 绝对口径(12.4/13.2 裁定):**link 装配不豁免**——四包 npm latest 必须 ∈ 豁免名单(防未来转非 link 装配踩坑 18)──
|
|
158
|
+
{
|
|
159
|
+
try {
|
|
160
|
+
const dshHome = process.env.DSH_HOME || join(process.env.USERPROFILE || "", ".dsh");
|
|
161
|
+
const wsYaml = readFileSync(join(dshHome, "profiles", "web", "pnpm-workspace.yaml"), "utf8");
|
|
162
|
+
const profPkg = JSON.parse(readFileSync(join(dshHome, "profiles", "web", "package.json"), "utf8"));
|
|
163
|
+
const pkgVerLocal = (p) => { try { return JSON.parse(readFileSync(join(dshHome, "profiles", "web", "node_modules", p, "package.json"), "utf8")).version; } catch { return null; } };
|
|
164
|
+
const fourPkgs = ["dsh-rule-engine", "dsh-rules-manager", "dsh-rules-manager-client", "dsh-rule-engine-client"];
|
|
165
|
+
const misses = [];
|
|
166
|
+
const viewFails = [];
|
|
167
|
+
// 豁免判定(单源:scripts/lib/pnpm-exempt.mjs——与 release-plugin 预插共享;一致性测试锁)
|
|
168
|
+
const exempt = (p, v) => exemptPkg(wsYaml, p, v);
|
|
169
|
+
for (const p of fourPkgs) {
|
|
170
|
+
const spec = profPkg.dependencies?.[p];
|
|
171
|
+
if (!spec) continue;
|
|
172
|
+
const isLink = typeof spec === "string" && spec.startsWith("link:");
|
|
173
|
+
const esc = p.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
174
|
+
// ⑬ 强校验(12.4 修法绝对口径):npm 已发布 latest 必须 ∈ 豁免名单——**不因 link 装配豁免**
|
|
175
|
+
//(link 包其 npm 已发布版本仍须豁免——防未来转非 link 装配时踩坑 18);本机版校验仅非 link 执行
|
|
176
|
+
let latest = null;
|
|
177
|
+
try {
|
|
178
|
+
const npmCli = join(dirname(process.execPath), "node_modules", "npm", "bin", "npm-cli.js");
|
|
179
|
+
latest = (process.platform === "win32" && existsSync(npmCli))
|
|
180
|
+
? execFileSync(process.execPath, [npmCli, "view", p, "version"], { encoding: "utf8", stdio: "pipe" }).trim()
|
|
181
|
+
: execFileSync("npm", ["view", p, "version"], { encoding: "utf8", stdio: "pipe" }).trim();
|
|
182
|
+
} catch { latest = null; viewFails.push(p); }
|
|
183
|
+
if (latest && !exempt(p, latest)) {
|
|
184
|
+
misses.push(`${p}@${latest}(npm latest 未豁免)`);
|
|
185
|
+
}
|
|
186
|
+
if (!isLink) {
|
|
187
|
+
const ver = pkgVerLocal(p);
|
|
188
|
+
if (ver && !exempt(p, ver)) misses.push(`${p}@${ver}(本机版未豁免)`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
if (process.env.VERIFY_ALL_TESTMODE === "1") { misses.push("TESTMODE 合成 miss(门禁活性自证——必须转红)"); viewFails.push("TESTMODE-合成"); }
|
|
192
|
+
if (viewFails.length) {
|
|
193
|
+
// E2:发布语境(VERIFY_ALL_STRICT=1,release-plugin 3.7 设置)viewFails>0 计 ❌(fail-closed);本地默认 ⚠️ 可见+抑制 ✅
|
|
194
|
+
const strictFail = process.env.VERIFY_ALL_STRICT === "1";
|
|
195
|
+
lines.push(strictFail
|
|
196
|
+
? `❌ pnpm 豁免校验:npm view 执行失败 ${viewFails.length} 包(${viewFails.join("、")})——发布语境 fail-closed(latest 未获取即阻断)`
|
|
197
|
+
: `⚠️ pnpm 豁免校验:npm view 执行失败 ${viewFails.length} 包(${viewFails.join("、")})——latest 未获取(fail-closed:未完成≠通过)`);
|
|
198
|
+
}
|
|
199
|
+
if (misses.length) {
|
|
200
|
+
lines.push(`❌ pnpm 豁免校验:${misses.join("、")} 不在 minimumReleaseAgeExclude(发布后首装会被静默跳过——见踩坑 18)`);
|
|
201
|
+
} else if (!viewFails.length) {
|
|
202
|
+
lines.push("✅ pnpm 豁免校验(四包 npm latest 均已豁免)");
|
|
203
|
+
}
|
|
204
|
+
} catch (e) {
|
|
205
|
+
lines.push("⚠️ pnpm 豁免校验:读取失败(本机 profile 缺失时跳过)");
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
133
209
|
console.log(lines.join("\n"));
|
|
134
210
|
const failed = lines.filter((l) => l.startsWith("❌"));
|
|
135
211
|
if (failed.length) {
|