dsh-rule-engine 0.3.0
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/LICENSE +21 -0
- package/README.md +132 -0
- package/cordis.patch.yml +7 -0
- package/lib/core/audit.js +44 -0
- package/lib/core/authorization.js +227 -0
- package/lib/core/config.js +30 -0
- package/lib/core/guard-core.js +453 -0
- package/lib/core/llm-understander.js +104 -0
- package/lib/core/matcher.js +50 -0
- package/lib/core/mount-signature.js +87 -0
- package/lib/core/parser.js +101 -0
- package/lib/core/paths.js +24 -0
- package/lib/core/patterns.js +351 -0
- package/lib/core/runtime.js +5 -0
- package/lib/core/silent-error.js +44 -0
- package/lib/core/state.js +185 -0
- package/lib/core/text-detect.js +185 -0
- package/lib/core/understander.js +127 -0
- package/lib/core/understanding-store.js +27 -0
- package/lib/core/version-guard.js +117 -0
- package/lib/index.js +648 -0
- package/lib/service.js +174 -0
- package/package.json +48 -0
- package/scripts/audit-mount-consistency.mjs +198 -0
- package/scripts/build.sh +13 -0
- package/scripts/check-real.mjs +24 -0
- package/upgrade-impact.json +55 -0
package/lib/service.js
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
// service.js —— 规则引擎 host 端 Remote 服务
|
|
2
|
+
// 供设置面板(dsh-rules-manager-client 的“规则引擎”页签)通过 ctx.remote.ruleEngine.* 调用。
|
|
3
|
+
// 与 rules-manager/service.js 同构:TypertRemoteService + 手动 @Remote 标记。
|
|
4
|
+
import { readFileSync } from "node:fs";
|
|
5
|
+
import { Remote, TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
|
|
6
|
+
import { state } from "./core/runtime.js";
|
|
7
|
+
import { readAuditLog } from "./core/audit.js";
|
|
8
|
+
import { loadPluginConfig } from "./core/config.js";
|
|
9
|
+
import { agentsFilePath, auditFilePath } from "./core/paths.js";
|
|
10
|
+
|
|
11
|
+
const REMOTE_METHODS = [
|
|
12
|
+
"getStatus",
|
|
13
|
+
"getVersion",
|
|
14
|
+
"checkUpdate",
|
|
15
|
+
"getAuditLog",
|
|
16
|
+
"getUnderstanding"
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
function currentVersion() {
|
|
20
|
+
try {
|
|
21
|
+
const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
22
|
+
return pkg.version || "0.0.0";
|
|
23
|
+
} catch {
|
|
24
|
+
return "0.0.0";
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function parseVersion(v) {
|
|
29
|
+
const s = String(v || "").replace(/^v/i, "");
|
|
30
|
+
const parts = s.split(".").map((n) => parseInt(n, 10) || 0);
|
|
31
|
+
return { major: parts[0] || 0, minor: parts[1] || 0, patch: parts[2] || 0 };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function isNewerVersion(a, b) {
|
|
35
|
+
const A = parseVersion(a);
|
|
36
|
+
const B = parseVersion(b);
|
|
37
|
+
if (B.major !== A.major) return B.major > A.major;
|
|
38
|
+
if (B.minor !== A.minor) return B.minor > A.minor;
|
|
39
|
+
return B.patch > A.patch;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
class RuleEngineService extends TypertRemoteService {
|
|
43
|
+
static inject = [];
|
|
44
|
+
|
|
45
|
+
constructor(ctx) {
|
|
46
|
+
super(ctx, "ruleEngine");
|
|
47
|
+
for (const name of REMOTE_METHODS) {
|
|
48
|
+
Remote(null, {
|
|
49
|
+
kind: "method",
|
|
50
|
+
name,
|
|
51
|
+
private: false,
|
|
52
|
+
static: false,
|
|
53
|
+
addInitializer: (fn) => {
|
|
54
|
+
fn.call(this);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** 引擎当前状态(版本/开关/规则数/审计路径/最近激活等) */
|
|
61
|
+
async getStatus() {
|
|
62
|
+
try {
|
|
63
|
+
const conf = loadPluginConfig();
|
|
64
|
+
return {
|
|
65
|
+
ok: true,
|
|
66
|
+
status: {
|
|
67
|
+
version: currentVersion(),
|
|
68
|
+
enabled: state.enabled,
|
|
69
|
+
configOk: state.configOk,
|
|
70
|
+
configError: state.configError || "",
|
|
71
|
+
rulesCount: state.configs.length,
|
|
72
|
+
mountRevision: state.mountRevision,
|
|
73
|
+
auditFile: auditFilePath(),
|
|
74
|
+
agentsFile: agentsFilePath(),
|
|
75
|
+
reloadCount: state.reloadCount,
|
|
76
|
+
lastActive: (state.lastActive || []).slice(0, 10)
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
} catch (error) {
|
|
80
|
+
return { ok: false, error: error instanceof Error ? error.message : String(error) };
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** 当前版本号 */
|
|
85
|
+
async getVersion() {
|
|
86
|
+
return { ok: true, version: currentVersion() };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* 检查 GitHub 最新 Release 与 upgrade-impact.json。
|
|
91
|
+
* 只读检查,不下载、不替换、不修改任何用户规则。
|
|
92
|
+
*/
|
|
93
|
+
async checkUpdate() {
|
|
94
|
+
try {
|
|
95
|
+
const current = currentVersion();
|
|
96
|
+
const headers = { "User-Agent": "dsh-rule-engine", Accept: "application/vnd.github+json" };
|
|
97
|
+
const releaseRes = await fetch("https://api.github.com/repos/jilian-dsh/dsh-rule-engine/releases/latest", { headers });
|
|
98
|
+
if (!releaseRes.ok) {
|
|
99
|
+
return { ok: false, error: `GitHub Release 检查失败:HTTP ${releaseRes.status}` };
|
|
100
|
+
}
|
|
101
|
+
const release = await releaseRes.json();
|
|
102
|
+
const latestTag = String(release.tag_name || "").replace(/^v/i, "");
|
|
103
|
+
const hasUpdate = isNewerVersion(current, latestTag);
|
|
104
|
+
|
|
105
|
+
let impacts = [];
|
|
106
|
+
try {
|
|
107
|
+
const impactRes = await fetch("https://raw.githubusercontent.com/jilian-dsh/dsh-rule-engine/main/upgrade-impact.json", { headers: { "User-Agent": "dsh-rule-engine" } });
|
|
108
|
+
if (impactRes.ok) {
|
|
109
|
+
const data = await impactRes.json();
|
|
110
|
+
if (Array.isArray(data?.versions)) {
|
|
111
|
+
impacts = data.versions
|
|
112
|
+
.filter((v) => v && isNewerVersion(current, String(v.version || "").replace(/^v/i, "")))
|
|
113
|
+
.sort((a, b) => isNewerVersion(String(a.version || ""), String(b.version || "")) ? 1 : -1);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
} catch {
|
|
117
|
+
// impact 文件拉取失败不影响 Release 检查结果
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
ok: true,
|
|
122
|
+
current,
|
|
123
|
+
latest: {
|
|
124
|
+
tag_name: release.tag_name || "",
|
|
125
|
+
name: release.name || "",
|
|
126
|
+
published_at: release.published_at || "",
|
|
127
|
+
html_url: release.html_url || "",
|
|
128
|
+
body: release.body || "",
|
|
129
|
+
assets: Array.isArray(release.assets) ? release.assets.map((a) => ({
|
|
130
|
+
name: a.name,
|
|
131
|
+
browser_download_url: a.browser_download_url,
|
|
132
|
+
size: a.size
|
|
133
|
+
})) : []
|
|
134
|
+
},
|
|
135
|
+
hasUpdate,
|
|
136
|
+
impacts
|
|
137
|
+
};
|
|
138
|
+
} catch (error) {
|
|
139
|
+
return { ok: false, error: error instanceof Error ? error.message : String(error) };
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** 最近审计日志(默认 10 条) */
|
|
144
|
+
async getAuditLog(n) {
|
|
145
|
+
try {
|
|
146
|
+
const count = Math.min(Math.max(1, Number(n) || 10), 200);
|
|
147
|
+
return { ok: true, entries: readAuditLog(count) };
|
|
148
|
+
} catch (error) {
|
|
149
|
+
return { ok: false, error: error instanceof Error ? error.message : String(error) };
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** 规则理解摘要(ruleId/title/level/handler/confidence/actions) */
|
|
154
|
+
async getUnderstanding() {
|
|
155
|
+
try {
|
|
156
|
+
return {
|
|
157
|
+
ok: true,
|
|
158
|
+
rules: (state.configs || []).map((c) => ({
|
|
159
|
+
ruleId: c.ruleId,
|
|
160
|
+
title: c.title,
|
|
161
|
+
level: c.level || "",
|
|
162
|
+
handler: c.handler || "",
|
|
163
|
+
confidence: c.confidence || "",
|
|
164
|
+
actions: c.actions || [],
|
|
165
|
+
disabled: c.disabled === true
|
|
166
|
+
}))
|
|
167
|
+
};
|
|
168
|
+
} catch (error) {
|
|
169
|
+
return { ok: false, error: error instanceof Error ? error.message : String(error) };
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export { RuleEngineService, RuleEngineService as default };
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "dsh-rule-engine",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "DSH 规则执行引擎 v3:容器解析 AGENTS.md + 理解器 + 匹配机 + 执行框架",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "lib/index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./lib/index.js",
|
|
9
|
+
"./lib/service.js": "./lib/service.js",
|
|
10
|
+
"./package.json": "./package.json"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"lib",
|
|
14
|
+
"scripts",
|
|
15
|
+
"upgrade-impact.json",
|
|
16
|
+
"cordis.patch.yml",
|
|
17
|
+
"README.md",
|
|
18
|
+
"LICENSE"
|
|
19
|
+
],
|
|
20
|
+
"keywords": [
|
|
21
|
+
"deepseek-harness",
|
|
22
|
+
"dsh",
|
|
23
|
+
"dsh-plugin",
|
|
24
|
+
"cordis",
|
|
25
|
+
"rules",
|
|
26
|
+
"guard",
|
|
27
|
+
"security",
|
|
28
|
+
"agent"
|
|
29
|
+
],
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "https://github.com/jilian-dsh/dsh-rule-engine.git"
|
|
34
|
+
},
|
|
35
|
+
"scripts": {
|
|
36
|
+
"test": "node test/run-all.js",
|
|
37
|
+
"check": "node --check lib/index.js",
|
|
38
|
+
"audit:mount": "node scripts/audit-mount-consistency.mjs --profile web"
|
|
39
|
+
},
|
|
40
|
+
"peerDependencies": {
|
|
41
|
+
"@deepseek-ai/dsh-typert-protocol": ">=0.0.1-rc.3"
|
|
42
|
+
},
|
|
43
|
+
"dsh": {
|
|
44
|
+
"bundle": {
|
|
45
|
+
"patch": "./cordis.patch.yml"
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
// FULL mount-consistency audit: bundles vs user patch inserts vs bundle-internal patches vs dependencies
|
|
2
|
+
// Goal: detect ANY loader entry id that would be mounted more than once (duplicate loader entry crash)
|
|
3
|
+
import { readFileSync, existsSync, readdirSync } from 'node:fs'
|
|
4
|
+
import { createRequire } from 'node:module'
|
|
5
|
+
import { join, dirname } from 'node:path'
|
|
6
|
+
|
|
7
|
+
const DSH_HOME = process.env.DSH_HOME || 'D:/DeepSeek harness/.dsh'
|
|
8
|
+
const DEFAULT_PROFILE = 'web'
|
|
9
|
+
function profileNameFromArgs() {
|
|
10
|
+
const argv = process.argv
|
|
11
|
+
let idx = argv.indexOf('--profile')
|
|
12
|
+
if (idx >= 0 && argv[idx + 1]) return argv[idx + 1]
|
|
13
|
+
const eq = argv.find((a) => a.startsWith('--profile='))
|
|
14
|
+
if (eq) return eq.slice('--profile='.length)
|
|
15
|
+
return DEFAULT_PROFILE
|
|
16
|
+
}
|
|
17
|
+
const PROFILE_NAME = profileNameFromArgs()
|
|
18
|
+
const PROFILE = join(DSH_HOME, 'profiles', PROFILE_NAME)
|
|
19
|
+
console.log(`Profile: ${PROFILE_NAME} -> ${PROFILE}`)
|
|
20
|
+
if (!existsSync(join(PROFILE, 'package.json'))) {
|
|
21
|
+
console.error(`[ERROR] profile package.json not found: ${join(PROFILE, 'package.json')}`)
|
|
22
|
+
process.exit(2)
|
|
23
|
+
}
|
|
24
|
+
const profilePkg = JSON.parse(readFileSync(join(PROFILE, 'package.json'), 'utf8'))
|
|
25
|
+
const requireFromProfile = createRequire(join(PROFILE, 'package.json'))
|
|
26
|
+
|
|
27
|
+
/** 从 patch 文本提取 insert 块中的 loader entry id(支持块状和行内两种写法) */
|
|
28
|
+
function extractInsertIds(text) {
|
|
29
|
+
const ids = []
|
|
30
|
+
const lines = String(text || '').split('\n')
|
|
31
|
+
for (let i = 0; i < lines.length; i++) {
|
|
32
|
+
const line = lines[i]
|
|
33
|
+
if (!/^-\s*insert:/i.test(line)) continue
|
|
34
|
+
const rest = line.replace(/^-\s*insert:\s*/i, '')
|
|
35
|
+
if (rest.trim()) {
|
|
36
|
+
// 行内形式:- insert: { id: xxx } / - insert: - id: xxx
|
|
37
|
+
for (const m of rest.matchAll(/id:\s*["']?([^\s#"']+)/gi)) ids.push(m[1])
|
|
38
|
+
continue
|
|
39
|
+
}
|
|
40
|
+
// 块状形式:- insert: 后跟缩进的 - id: xxx
|
|
41
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
42
|
+
const l = lines[j]
|
|
43
|
+
if (/^\S/.test(l) && !/^\s/.test(l)) break // 回到顶层条目
|
|
44
|
+
const m = l.match(/^\s+- id:\s*["']?([^\s#"']+)/i)
|
|
45
|
+
if (m) ids.push(m[1])
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return [...new Set(ids)]
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** 解析 bundle 包路径:优先 profile/node_modules,再 profiles/node_modules,最后用 Node 解析 */
|
|
52
|
+
function resolveBundlePkg(b) {
|
|
53
|
+
const candidates = [
|
|
54
|
+
join(PROFILE, 'node_modules', b, 'package.json'),
|
|
55
|
+
join(DSH_HOME, 'profiles', 'node_modules', b, 'package.json')
|
|
56
|
+
]
|
|
57
|
+
if (b.startsWith('@')) {
|
|
58
|
+
const [scope, name] = b.split('/')
|
|
59
|
+
candidates.push(join(DSH_HOME, 'profiles', 'node_modules', scope, name, 'package.json'))
|
|
60
|
+
}
|
|
61
|
+
for (const c of candidates) if (existsSync(c)) return c
|
|
62
|
+
try {
|
|
63
|
+
return requireFromProfile.resolve(`${b}/package.json`)
|
|
64
|
+
} catch {
|
|
65
|
+
return null
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** 读取一个 patch 文件并把其中的 insert id 记入 sources */
|
|
70
|
+
function collectPatchInserts(patchPath, viaLabel) {
|
|
71
|
+
if (!existsSync(patchPath)) return []
|
|
72
|
+
const ids = extractInsertIds(readFileSync(patchPath, 'utf8'))
|
|
73
|
+
for (const id of ids) sources.push({ id, via: viaLabel, file: patchPath })
|
|
74
|
+
return ids
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ---- collect all sources of loader entry ids ----
|
|
78
|
+
const sources = [] // {id, via, file}
|
|
79
|
+
|
|
80
|
+
// 1. profile bundles array (each bundle's internal cordis.patch.yml inserts + the bundle row itself)
|
|
81
|
+
const bundles = profilePkg.dsh?.profile?.bundles || []
|
|
82
|
+
console.log('=== 1. profile bundles array (' + bundles.length + ') ===')
|
|
83
|
+
for (const b of bundles) {
|
|
84
|
+
console.log(' bundle:', b)
|
|
85
|
+
const pkgPath = resolveBundlePkg(b)
|
|
86
|
+
if (!pkgPath) { console.log(' [WARN] package.json not found for ' + b); continue }
|
|
87
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'))
|
|
88
|
+
const patchRel = pkg.dsh?.bundle?.patch
|
|
89
|
+
if (patchRel) {
|
|
90
|
+
const patchPath = join(dirname(pkgPath), patchRel)
|
|
91
|
+
const inserts = collectPatchInserts(patchPath, `bundle-internal patch of ${b}`)
|
|
92
|
+
if (existsSync(patchPath)) {
|
|
93
|
+
console.log(` internal patch ${patchRel}: inserts = ${inserts.length ? inserts.join(',') : '(none)'}`)
|
|
94
|
+
} else {
|
|
95
|
+
console.log(' [WARN] bundle patch missing: ' + patchPath)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
// the bundle package itself may be a plugin row too (via loader) — record its name as entry candidate
|
|
99
|
+
// (bundle packages themselves are not loader entries; only their patch rows are)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// 2. user cordis.patch.yml inserts (profile layer)
|
|
103
|
+
const userPatchPath = join(PROFILE, 'cordis.patch.yml')
|
|
104
|
+
const userPatch = readFileSync(userPatchPath, 'utf8')
|
|
105
|
+
console.log('\n=== 2. user cordis.patch.yml (profile layer) ===')
|
|
106
|
+
const userInserts = collectPatchInserts(userPatchPath, 'user patch insert')
|
|
107
|
+
// also non-insert top-level entries (override/disable by id) — these do NOT create entries but list them
|
|
108
|
+
const topLevelIds = [...userPatch.matchAll(/^-\s+id:\s+([^\s#]+)/gm)].map((m) => m[1])
|
|
109
|
+
console.log(' user patch top-level ids:', topLevelIds.join(',') || '(none)')
|
|
110
|
+
console.log(' user patch inserts:', userInserts.join(',') || '(none)')
|
|
111
|
+
|
|
112
|
+
// 2.5 root user cordis.patch.yml inserts (if present)
|
|
113
|
+
const rootPatchPath = join(DSH_HOME, 'cordis.patch.yml')
|
|
114
|
+
const rootInserts = []
|
|
115
|
+
if (existsSync(rootPatchPath)) {
|
|
116
|
+
console.log('\n=== 2.5 root user cordis.patch.yml ===')
|
|
117
|
+
rootInserts.push(...collectPatchInserts(rootPatchPath, 'root user patch insert'))
|
|
118
|
+
console.log(' root user patch inserts:', rootInserts.join(',') || '(none)')
|
|
119
|
+
} else {
|
|
120
|
+
console.log('\n=== 2.5 root user cordis.patch.yml ===')
|
|
121
|
+
console.log(' (no root cordis.patch.yml)')
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// 3. dependencies with link/github/http (local dev packages) — check their type
|
|
125
|
+
console.log('\n=== 3. dependencies type check ===')
|
|
126
|
+
const deps = profilePkg.dependencies || {}
|
|
127
|
+
for (const [name, spec] of Object.entries(deps)) {
|
|
128
|
+
const isLocal = typeof spec === 'string' && (spec.startsWith('link:') || spec.startsWith('github:') || spec.startsWith('http') || spec.startsWith('file:'))
|
|
129
|
+
console.log(` ${name}: ${spec} ${isLocal ? '(LOCAL/dev)' : ''}`)
|
|
130
|
+
if (isLocal) {
|
|
131
|
+
// resolve local dir and check dsh type
|
|
132
|
+
let dir = null
|
|
133
|
+
if (spec.startsWith('link:')) dir = spec.slice(5)
|
|
134
|
+
else if (spec.startsWith('file:')) dir = spec.slice(5)
|
|
135
|
+
else if (spec.startsWith('github:') || spec.startsWith('http')) dir = join(PROFILE, 'node_modules', name)
|
|
136
|
+
if (dir) {
|
|
137
|
+
const lp = join(dir.replace(/\\/g, '/'), 'package.json')
|
|
138
|
+
if (existsSync(lp)) {
|
|
139
|
+
const lpj = JSON.parse(readFileSync(lp, 'utf8'))
|
|
140
|
+
const dshType = lpj.dsh?.bundle ? 'dsh.bundle' : (lpj.dsh?.plugin ? 'dsh.plugin' : (lpj.dsh ? JSON.stringify(lpj.dsh) : 'no dsh field'))
|
|
141
|
+
const inBundles = bundles.includes(name)
|
|
142
|
+
const inUserPatch = userInserts.includes(name) || rootInserts.includes(name) || topLevelIds.includes(name)
|
|
143
|
+
console.log(` -> type: ${dshType} | inBundles: ${inBundles} | inUserPatch: ${inUserPatch}`)
|
|
144
|
+
// rule 24: dsh.plugin must NOT be in bundles; dsh.bundle SHOULD be in bundles not in user patch insert
|
|
145
|
+
if (dshType === 'dsh.plugin' && inBundles) {
|
|
146
|
+
console.log(' [VIOLATION rule24] dsh.plugin in bundles!')
|
|
147
|
+
}
|
|
148
|
+
if (dshType === 'dsh.bundle' && inUserPatch) {
|
|
149
|
+
console.log(' [WARN] dsh.bundle also present in user patch — check duplicate')
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// 3.5 runtime injection registry (super-injector) — also produces loader entries
|
|
157
|
+
console.log('\n=== 3.5 runtime injection registry ===')
|
|
158
|
+
const superInjectorRegistry = join(DSH_HOME, 'super-injector', 'registry.json')
|
|
159
|
+
try {
|
|
160
|
+
if (existsSync(superInjectorRegistry)) {
|
|
161
|
+
const reg = JSON.parse(readFileSync(superInjectorRegistry, 'utf8'))
|
|
162
|
+
if (!Array.isArray(reg)) throw new Error('registry.json is not an array')
|
|
163
|
+
const names = reg.map((e) => e && e.name).filter(Boolean)
|
|
164
|
+
for (const name of names) {
|
|
165
|
+
sources.push({ id: name, via: 'runtime injection (super-injector registry)', file: superInjectorRegistry })
|
|
166
|
+
}
|
|
167
|
+
console.log(' runtime injected:', names.join(', ') || '(none)')
|
|
168
|
+
} else {
|
|
169
|
+
console.log(' (no super-injector registry found; if dev_inject_plugin has been used, run dev_injected_list to confirm)')
|
|
170
|
+
}
|
|
171
|
+
} catch (e) {
|
|
172
|
+
console.log(' [WARN] failed to read super-injector registry:', e instanceof Error ? e.message : String(e))
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// 4. duplicate detection across ALL sources
|
|
176
|
+
console.log('\n=== 4. DUPLICATE LOADER ENTRY DETECTION ===')
|
|
177
|
+
const byId = new Map()
|
|
178
|
+
for (const s of sources) {
|
|
179
|
+
if (!byId.has(s.id)) byId.set(s.id, [])
|
|
180
|
+
byId.get(s.id).push(s)
|
|
181
|
+
}
|
|
182
|
+
let dupFound = false
|
|
183
|
+
for (const [id, list] of byId) {
|
|
184
|
+
if (list.length > 1) {
|
|
185
|
+
dupFound = true
|
|
186
|
+
console.log(` [DUPLICATE] id "${id}" mounted ${list.length} times:`)
|
|
187
|
+
for (const s of list) console.log(` - via ${s.via} (${s.file})`)
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
if (!dupFound) console.log(' NO duplicate loader entry ids found — clean')
|
|
191
|
+
|
|
192
|
+
// 5. also check bundle rows vs their internal patch first insert (self-reference is fine)
|
|
193
|
+
console.log('\n=== 5. summary ===')
|
|
194
|
+
console.log(' bundles:', bundles.join(', '))
|
|
195
|
+
console.log(' user inserts:', userInserts.join(',') || '(none)')
|
|
196
|
+
console.log(' root user inserts:', rootInserts.join(',') || '(none)')
|
|
197
|
+
console.log(dupFound ? ' RESULT: DUPLICATES FOUND — MUST FIX BEFORE RESTART' : ' RESULT: MOUNT CONSISTENT — SAFE TO RESTART')
|
|
198
|
+
process.exit(dupFound ? 1 : 0)
|
package/scripts/build.sh
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
cd "$(dirname "$0")/.."
|
|
4
|
+
node --check lib/index.js
|
|
5
|
+
node --check lib/core/parser.js
|
|
6
|
+
node --check lib/core/understander.js
|
|
7
|
+
node --check lib/core/patterns.js
|
|
8
|
+
node --check lib/core/state.js
|
|
9
|
+
node --check lib/core/audit.js
|
|
10
|
+
node --check lib/core/guard-core.js
|
|
11
|
+
node --check lib/core/text-detect.js
|
|
12
|
+
node --check lib/core/matcher.js
|
|
13
|
+
echo "build check ok"
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// check-real.mjs - 只读校验:用真实 ~/.dsh/AGENTS.md 跑解析+理解,不写任何文件。
|
|
2
|
+
import { loadRules } from "../lib/core/parser.js";
|
|
3
|
+
import { understandAll } from "../lib/core/understander.js";
|
|
4
|
+
|
|
5
|
+
const parsed = loadRules();
|
|
6
|
+
if (!parsed.ok) {
|
|
7
|
+
console.error(JSON.stringify({ ok: false, error: parsed.error }, null, 2));
|
|
8
|
+
process.exit(1);
|
|
9
|
+
}
|
|
10
|
+
const configs = understandAll(parsed.rules);
|
|
11
|
+
const summary = {
|
|
12
|
+
ok: true,
|
|
13
|
+
ruleCount: parsed.rules.length,
|
|
14
|
+
sections: [...new Set(parsed.rules.map((r) => r.section))],
|
|
15
|
+
confidence: configs.reduce((acc, c) => {
|
|
16
|
+
acc[c.confidence] = (acc[c.confidence] || 0) + 1;
|
|
17
|
+
return acc;
|
|
18
|
+
}, {}),
|
|
19
|
+
actions: configs.reduce((acc, c) => {
|
|
20
|
+
for (const a of c.actions) acc[a] = (acc[a] || 0) + 1;
|
|
21
|
+
return acc;
|
|
22
|
+
}, {})
|
|
23
|
+
};
|
|
24
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"versions": [
|
|
3
|
+
{
|
|
4
|
+
"version": "0.2.1",
|
|
5
|
+
"summary": "version-guard 放行非表格单行修改(整行替换但行首锚点一致),避免 edit 工具修改规则正文被误回滚",
|
|
6
|
+
"impacts": [
|
|
7
|
+
{
|
|
8
|
+
"rule": "25",
|
|
9
|
+
"level": "behavior-change",
|
|
10
|
+
"description": "validateEditAppend 新增非表格单行替换放行:行首锚点足够长且规则编号一致时允许整行修改;版本表格行仍保持仅重编号放行。"
|
|
11
|
+
}
|
|
12
|
+
],
|
|
13
|
+
"userRulesUnaffected": true,
|
|
14
|
+
"notes": "升级只更新规则引擎插件本身,不修改 AGENTS.md / SKILL.md / 用户自定义规则。"
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"version": "0.2.0",
|
|
18
|
+
"summary": "规则引擎设置面板、升级检查与影响展示;规则 19/21/22/24/25/27 执行口径升级",
|
|
19
|
+
"impacts": [
|
|
20
|
+
{
|
|
21
|
+
"rule": "19",
|
|
22
|
+
"level": "behavior-change",
|
|
23
|
+
"description": "知识沉淀载体从“仅手册”扩展为可选技能/脚本/仓库文档等;不改变用户已写规则文本。"
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"rule": "21",
|
|
27
|
+
"level": "behavior-change",
|
|
28
|
+
"description": "规则审批改为分级确认,并新增“选项即边界”;不改变既有规则语义。"
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"rule": "22",
|
|
32
|
+
"level": "behavior-change",
|
|
33
|
+
"description": "被指出错误时需主动给出原因/改正/防再犯;引擎新增 D 级自证提醒。"
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"rule": "27",
|
|
37
|
+
"level": "behavior-change",
|
|
38
|
+
"description": "装配变更检测从会话级改为全局 mountRevision + 本会话审计证据;其他会话未审计也会被拦。"
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"rule": "24",
|
|
42
|
+
"level": "behavior-change",
|
|
43
|
+
"description": "新增手工编辑 profile package.json 的 dsh.profile.bundles 类型检查。"
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"rule": "25",
|
|
47
|
+
"level": "behavior-change",
|
|
48
|
+
"description": "新增 dev_stage_add/call/promote/demote 通用执行器纳入覆盖与敏感授权检查。"
|
|
49
|
+
}
|
|
50
|
+
],
|
|
51
|
+
"userRulesUnaffected": true,
|
|
52
|
+
"notes": "升级只更新规则引擎插件本身,不修改 AGENTS.md / SKILL.md / 用户自定义规则。"
|
|
53
|
+
}
|
|
54
|
+
]
|
|
55
|
+
}
|