dsh-rule-engine 0.5.17 → 0.6.1
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 +268 -236
- package/lib/core/authorization.js +68 -13
- package/lib/core/contract.js +1 -1
- package/lib/core/guard-core.js +46 -22
- package/lib/core/intent.js +322 -322
- package/lib/core/mount-signature.js +87 -87
- package/lib/core/patterns.js +744 -730
- package/lib/core/state.js +2 -1
- package/lib/core/text-detect.js +24 -4
- package/lib/core/tool-catalog.js +3 -0
- package/lib/index.js +101 -13
- package/lib/service.js +5 -2
- package/package.json +60 -58
- package/scripts/audit-mount-consistency.mjs +198 -198
- package/scripts/check-tool-coverage.mjs +63 -41
- package/scripts/lib/pnpm-exempt.mjs +56 -0
- package/scripts/local-residue-scan.mjs +40 -0
- package/scripts/publish-aptitude-check.mjs +102 -144
- package/scripts/readme-version-check.mjs +41 -0
- package/scripts/release-plugin.mjs +371 -329
- package/scripts/verify-all.mjs +85 -2
|
@@ -1,198 +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:/
|
|
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 (
|
|
157
|
-
console.log('\n=== 3.5 runtime injection registry ===')
|
|
158
|
-
const superInjectorRegistry = join(DSH_HOME, '
|
|
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 (
|
|
166
|
-
}
|
|
167
|
-
console.log(' runtime injected:', names.join(', ') || '(none)')
|
|
168
|
-
} else {
|
|
169
|
-
console.log(' (no
|
|
170
|
-
}
|
|
171
|
-
} catch (e) {
|
|
172
|
-
console.log(' [WARN] failed to read
|
|
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)
|
|
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:/example workspace/.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 (example-injector) — also produces loader entries
|
|
157
|
+
console.log('\n=== 3.5 runtime injection registry ===')
|
|
158
|
+
const superInjectorRegistry = join(DSH_HOME, 'example-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 (example-injector registry)', file: superInjectorRegistry })
|
|
166
|
+
}
|
|
167
|
+
console.log(' runtime injected:', names.join(', ') || '(none)')
|
|
168
|
+
} else {
|
|
169
|
+
console.log(' (no example-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 example-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)
|
|
@@ -1,41 +1,63 @@
|
|
|
1
|
-
// check-tool-coverage.mjs — 0.5.9 工具覆盖门禁(K-01/K-06 依据,仿官方 verify-tool-catalog):
|
|
2
|
-
// 官方 tool-catalog(生成器产物=权威全集)中的每个工具名都必须在分类表/前缀规则内被识别,
|
|
3
|
-
// 任何一个 unknown = 该工具调用会被未知工具首调处置(ask/deny)→ 工程失败。
|
|
4
|
-
//
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
1
|
+
// check-tool-coverage.mjs — 0.5.9 工具覆盖门禁(K-01/K-06 依据,仿官方 verify-tool-catalog):
|
|
2
|
+
// 官方 tool-catalog(生成器产物=权威全集)中的每个工具名都必须在分类表/前缀规则内被识别,
|
|
3
|
+
// 任何一个 unknown = 该工具调用会被未知工具首调处置(ask/deny)→ 工程失败。
|
|
4
|
+
// 0.6.0(02 B2′ 裁决 v1.4,2026-09-04 第三方裁示+用户确认):
|
|
5
|
+
// - 素材来源优先级:--catalog <path> → CHECK_TOOL_CATALOG 环境变量 → 均缺 = FAIL(诊断含获取方式,禁 ENOENT 死路径);
|
|
6
|
+
// - --skip-catalog 显式出口:输出 WARNING 并计入汇总(发布流水线永远不传该参数);
|
|
7
|
+
// - 素材版本须与本机 DSH 版本对齐(不一致 WARNING 不 FAIL);
|
|
8
|
+
// - 校验逻辑与判据不变(官方全集 ⊆ 分类表,任一 unknown 即红)。
|
|
9
|
+
// 用法:node scripts/check-tool-coverage.mjs [--catalog <path>] [--skip-catalog]
|
|
10
|
+
import { readFileSync } from "node:fs";
|
|
11
|
+
import { toolClass } from "../lib/core/tool-catalog.js";
|
|
12
|
+
|
|
13
|
+
function argVal(name) {
|
|
14
|
+
const i = process.argv.indexOf(name);
|
|
15
|
+
return i !== -1 && process.argv[i + 1] ? process.argv[i + 1] : null;
|
|
16
|
+
}
|
|
17
|
+
const SKIP_CATALOG = process.argv.includes("--skip-catalog");
|
|
18
|
+
const CATALOG = argVal("--catalog") || process.env.CHECK_TOOL_CATALOG || null;
|
|
19
|
+
|
|
20
|
+
if (!CATALOG) {
|
|
21
|
+
if (SKIP_CATALOG) {
|
|
22
|
+
console.warn("WARNING:--skip-catalog 已传——工具箱覆盖层跳过(素材缺失;发布流水线禁止此参数)");
|
|
23
|
+
process.exit(0);
|
|
24
|
+
}
|
|
25
|
+
console.error("TOOL-CATALOG-MISSING:未指定官方 tool-catalog.txt——请用 --catalog <path> 或环境变量 CHECK_TOOL_CATALOG 指定(官方文档产物可从 DSH 官方文档仓库 docs-site-text 获取);FAIL,禁止静默跳过");
|
|
26
|
+
if (process.env.LOCAL_CATALOG_HINT) {
|
|
27
|
+
console.error(`(本机提示:${process.env.LOCAL_CATALOG_HINT})`);
|
|
28
|
+
}
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function parseNames() {
|
|
33
|
+
const names = new Set();
|
|
34
|
+
for (const line of readFileSync(CATALOG, "utf8").split("\n")) {
|
|
35
|
+
if (!line.includes("@deepseek-ai/dsh-")) continue;
|
|
36
|
+
const cols = line.split("|").map((c) => c.trim());
|
|
37
|
+
// 表列格式:| | <package> | <model-visible names> | <requires> | <writes> | <shipped aliases> | <note> |
|
|
38
|
+
if (!cols[2] || !cols[2].startsWith("@deepseek-ai")) continue;
|
|
39
|
+
const add = (s) => {
|
|
40
|
+
if (!s || s === "-") return;
|
|
41
|
+
for (const x of s.split(",")) {
|
|
42
|
+
const n = x.trim();
|
|
43
|
+
if (/^[A-Za-z_][A-Za-z0-9_:.-]*$/.test(n)) names.add(n);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
add(cols[3]);
|
|
47
|
+
add(cols[6]);
|
|
48
|
+
}
|
|
49
|
+
return names;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const official = parseNames();
|
|
53
|
+
const missing = [];
|
|
54
|
+
for (const n of official) {
|
|
55
|
+
if (toolClass(n, {}) === "unknown") missing.push(n);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (missing.length) {
|
|
59
|
+
console.error(`COVERAGE-FAIL:官方 tool-catalog 有 ${missing.length} 个工具未分类(会被未知工具首调处置):`);
|
|
60
|
+
for (const n of missing) console.error(" - " + n);
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
console.log(`COVERAGE-OK:官方 tool-catalog 工具全覆盖(${official.size} 个工具名均被分类表/前缀规则识别)`);
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// local-residue-scan.mjs — 发布物本机痕迹扫描(仅扫 lib/;无 --pack 模式)
|
|
3
|
+
// 用法:node scripts/local-residue-scan.mjs (在包根目录运行;exit 0 = 干净,exit 1 = 有命中)
|
|
4
|
+
|
|
5
|
+
import { readFileSync, readdirSync, statSync } from "node:fs";
|
|
6
|
+
import { join, extname } from "node:path";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
import { dirname } from "node:path";
|
|
9
|
+
|
|
10
|
+
const root = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
11
|
+
|
|
12
|
+
// 词表唯一源(v1.2):scripts/local-residue-markers.txt,与阶段 A 的 A6 验收共用同一文件。
|
|
13
|
+
const MARKERS = readFileSync(join(root, "scripts", "local-residue-markers.txt"), "utf8")
|
|
14
|
+
.split("\n").map((s) => s.trim()).filter((s) => s && !s.startsWith("#"));
|
|
15
|
+
// 注意:不扫 package.json(作者署名/仓库地址为合法项,A6 口径见其节);tool-catalog.js 的
|
|
16
|
+
// dev_/esr_/engram_ 前缀规则按阶段 A「关键裁决 3」保留,静态枚举列 0.6.x 跟进。
|
|
17
|
+
|
|
18
|
+
const TEXT_EXT = new Set([".js", ".mjs", ".cjs", ".json", ".md", ".yml", ".yaml"]);
|
|
19
|
+
function* walk(dir) {
|
|
20
|
+
for (const e of readdirSync(dir)) {
|
|
21
|
+
const p = join(dir, e);
|
|
22
|
+
const s = statSync(p);
|
|
23
|
+
if (s.isDirectory()) { if (e !== "node_modules" && e !== ".git") yield* walk(p); }
|
|
24
|
+
else if (TEXT_EXT.has(extname(e))) yield p;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let hits = 0;
|
|
29
|
+
for (const file of walk(join(root, "lib"))) {
|
|
30
|
+
const text = readFileSync(file, "utf8");
|
|
31
|
+
for (const m of MARKERS) {
|
|
32
|
+
const lines = text.split("\n");
|
|
33
|
+
lines.forEach((line, i) => {
|
|
34
|
+
if (line.includes(m)) { console.log(`HIT ${file}:${i + 1} [${m}] ${line.trim()}`); hits++; }
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (hits) { console.error(`\nRESIDUE SCAN FAILED(${hits} 处本机痕迹)`); process.exit(1); }
|
|
40
|
+
console.log("RESIDUE SCAN OK(发布面零本机痕迹)");
|