dsh-standards 1.3.0 → 1.4.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/40-checker/scripts/build-sidecar.mjs +238 -8
- package/40-checker/src/author-cli.mjs +274 -7
- package/40-checker/src/author-report.mjs +14 -3
- package/40-checker/src/bundle-static.mjs +181 -0
- package/40-checker/src/cli.mjs +27 -3
- package/40-checker/src/client-shape.mjs +33 -3
- package/40-checker/src/declarations.mjs +53 -8
- package/40-checker/src/index.mjs +3 -1
- package/40-checker/src/manifest.mjs +87 -14
- package/40-checker/src/remediation.mjs +108 -7
- package/40-checker/src/report.mjs +83 -7
- package/40-checker/src/semver.mjs +112 -65
- package/40-checker/src/util.mjs +51 -8
- package/CHANGELOG.md +23 -0
- package/README.md +28 -16
- package/docs/coverage.md +82 -0
- package/package.json +1 -1
- package/spec/coverage-contract.md +100 -0
- package/spec/dsc-clauses.json +766 -0
- package/spec/examples/boost-card.example.json +15 -0
- package/spec/examples/flow-evidence.example.json +113 -0
- package/spec/remediation.json +54 -54
- package/spec/schemas/boost-card.schema.json +107 -0
- package/spec/schemas/conformance.schema.json +848 -0
|
@@ -1,17 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Minimal SemVer range matcher for the checker.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* The host evaluates plugin requirements with
|
|
5
5
|
* `semver.satisfies(runtime, range, { includePrerelease: true })`
|
|
6
|
-
* (deepseek-harness/packages/boot/app-boot/src/plugin-compatibility.ts
|
|
7
|
-
* This module implements the subset that appears in package manifests
|
|
8
|
-
* exact versions, `>= > <=
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
6
|
+
* (deepseek-harness/packages/boot/app-boot/src/plugin-compatibility.ts).
|
|
7
|
+
* This module implements the subset that appears in package manifests and in
|
|
8
|
+
* npm documentation: exact versions, `>= > <= < =`, `^`, `~`, `*`/`x`,
|
|
9
|
+
* space-separated AND (including `>= 0.2.2-rc.1`, where npm allows whitespace
|
|
10
|
+
* between an operator and its version), `||` alternatives, hyphen ranges and
|
|
11
|
+
* partial/x-ranges. Prereleases participate in ranges everywhere, matching the
|
|
12
|
+
* host's `includePrerelease: true`.
|
|
13
|
+
*
|
|
14
|
+
* It does NOT implement `!=` or npm's "prerelease only matches the same
|
|
15
|
+
* [major,minor,patch] tuple" exclusion: the host explicitly includes
|
|
16
|
+
* prereleases, so the checker must use the same rule.
|
|
12
17
|
*/
|
|
13
18
|
|
|
14
19
|
const VERSION_RE = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?$/
|
|
20
|
+
const OPERATORS = new Set(['>=', '<=', '>', '<', '=', '^', '~'])
|
|
15
21
|
|
|
16
22
|
export function parseVersion(value) {
|
|
17
23
|
if (typeof value !== 'string') return null
|
|
@@ -53,18 +59,26 @@ export function compareVersions(left, right) {
|
|
|
53
59
|
return 0
|
|
54
60
|
}
|
|
55
61
|
|
|
62
|
+
// npm encodes an exclusive upper bound as `X.Y.Z-0`, which also excludes
|
|
63
|
+
// prereleases of the boundary version. This matters for `^0.2.2-alpha.2`:
|
|
64
|
+
// 0.3.0-0 must NOT satisfy it, while `<0.3.0` (an explicit user range) does.
|
|
65
|
+
function sentinel(version) {
|
|
66
|
+
return { ...version, prerelease: ['0'] }
|
|
67
|
+
}
|
|
68
|
+
|
|
56
69
|
function upperForCaret(version) {
|
|
57
|
-
if (version.major > 0) return { ...version, major: version.major + 1, minor: 0, patch: 0
|
|
58
|
-
if (version.minor > 0) return { ...version, minor: version.minor + 1, patch: 0
|
|
59
|
-
return { ...version, patch: version.patch + 1
|
|
70
|
+
if (version.major > 0) return sentinel({ ...version, major: version.major + 1, minor: 0, patch: 0 })
|
|
71
|
+
if (version.minor > 0) return sentinel({ ...version, minor: version.minor + 1, patch: 0 })
|
|
72
|
+
return sentinel({ ...version, patch: version.patch + 1 })
|
|
60
73
|
}
|
|
61
74
|
|
|
62
75
|
function upperForTilde(version) {
|
|
63
|
-
return { ...version, minor: version.minor + 1, patch: 0
|
|
76
|
+
return sentinel({ ...version, minor: version.minor + 1, patch: 0 })
|
|
64
77
|
}
|
|
65
78
|
|
|
79
|
+
/** A version with optional missing minor/patch and optional prerelease. */
|
|
66
80
|
function parsePartial(text) {
|
|
67
|
-
const match = /^v?(\d+)(?:\.(\d+|[xX*]))?(?:\.(\d+|[xX*]))?(?:-([0-9A-Za-z.-]+))?$/.exec(text.trim())
|
|
81
|
+
const match = /^v?(\d+)(?:\.(\d+|[xX*]))?(?:\.(\d+|[xX*]))?(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/.exec(text.trim())
|
|
68
82
|
if (!match) return null
|
|
69
83
|
return {
|
|
70
84
|
major: Number(match[1]),
|
|
@@ -77,76 +91,112 @@ function parsePartial(text) {
|
|
|
77
91
|
function fullVersion(partial) {
|
|
78
92
|
return { major: partial.major, minor: partial.minor ?? 0, patch: partial.patch ?? 0, prerelease: partial.prerelease }
|
|
79
93
|
}
|
|
80
|
-
function nextMinor(partial) { return { major: partial.major, minor: (partial.minor ?? 0) + 1, patch: 0, prerelease: [] } }
|
|
81
|
-
function nextMajor(partial) { return { major: partial.major + 1, minor: 0, patch: 0, prerelease: [] } }
|
|
94
|
+
function nextMinor(partial) { return { major: partial.major, minor: (partial.minor ?? 0) + 1, patch: 0, prerelease: ['0'] } }
|
|
95
|
+
function nextMajor(partial) { return { major: partial.major + 1, minor: 0, patch: 0, prerelease: ['0'] } }
|
|
82
96
|
function fmt(version) {
|
|
83
97
|
const base = `${version.major}.${version.minor}.${version.patch}`
|
|
84
98
|
return version.prerelease && version.prerelease.length ? `${base}-${version.prerelease.join('.')}` : base
|
|
85
99
|
}
|
|
86
100
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
patch: match[4] === undefined || /[xX*]/.test(match[4]) ? null : Number(match[4]),
|
|
96
|
-
prerelease: match[5] ? match[5].split('.') : [],
|
|
101
|
+
/**
|
|
102
|
+
* Expand one npm comparator (`^1.2`, `>= 0.2.2-rc.1`, `1.x`, ...) into the
|
|
103
|
+
* primitive {@link satisfiesToken} comparators. Returns null when the token is
|
|
104
|
+
* not a range this checker can evaluate.
|
|
105
|
+
*/
|
|
106
|
+
function comparatorsForToken(op, versionText) {
|
|
107
|
+
if (versionText === '*' || versionText === 'x' || versionText === 'X') {
|
|
108
|
+
return op === '=' || op === undefined ? [] : null
|
|
97
109
|
}
|
|
110
|
+
const partial = parsePartial(versionText)
|
|
111
|
+
if (!partial) return null
|
|
112
|
+
const normalized = op ?? '='
|
|
113
|
+
if (!OPERATORS.has(normalized)) return null
|
|
98
114
|
const low = fullVersion(partial)
|
|
99
115
|
const noMinor = partial.minor === null
|
|
100
116
|
const noPatch = partial.patch === null
|
|
101
|
-
|
|
102
|
-
|
|
117
|
+
|
|
118
|
+
if (!noMinor && !noPatch) {
|
|
119
|
+
const exact = parseVersion(fmt(low))
|
|
120
|
+
if (!exact) return null
|
|
121
|
+
if (normalized === '^') return [{ op: '>=', version: exact }, { op: '<', version: upperForCaret(low) }]
|
|
122
|
+
if (normalized === '~') return [{ op: '>=', version: exact }, { op: '<', version: upperForTilde(low) }]
|
|
123
|
+
return [{ op: normalized, version: exact }]
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Partial / x-range. Under includePrerelease npm gives the *lower* bound a
|
|
127
|
+
// `-0` sentinel too, so `^1.2` matches 1.2.0-0 while `>=1.2.3` does not.
|
|
128
|
+
const lowSentinel = sentinel(low)
|
|
129
|
+
switch (normalized) {
|
|
103
130
|
case '^':
|
|
131
|
+
// ^1.2 -> <2.0.0-0, ^0.2 -> <0.3.0-0, ^0.0 -> <0.1.0-0
|
|
132
|
+
return noMinor || partial.major > 0
|
|
133
|
+
? [{ op: '>=', version: lowSentinel }, { op: '<', version: nextMajor(partial) }]
|
|
134
|
+
: [{ op: '>=', version: lowSentinel }, { op: '<', version: nextMinor(partial) }]
|
|
104
135
|
case '~':
|
|
136
|
+
return noMinor
|
|
137
|
+
? [{ op: '>=', version: lowSentinel }, { op: '<', version: nextMajor(partial) }]
|
|
138
|
+
: [{ op: '>=', version: lowSentinel }, { op: '<', version: nextMinor(partial) }]
|
|
105
139
|
case '=':
|
|
106
|
-
return noMinor
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
case '
|
|
110
|
-
case '
|
|
140
|
+
return noMinor
|
|
141
|
+
? [{ op: '>=', version: lowSentinel }, { op: '<', version: nextMajor(partial) }]
|
|
142
|
+
: [{ op: '>=', version: lowSentinel }, { op: '<', version: nextMinor(partial) }]
|
|
143
|
+
case '>=': return [{ op: '>=', version: lowSentinel }]
|
|
144
|
+
case '>': return noMinor ? [{ op: '>=', version: nextMajor(partial) }] : [{ op: '>=', version: nextMinor(partial) }]
|
|
145
|
+
case '<': return [{ op: '<', version: lowSentinel }]
|
|
146
|
+
case '<=': return noMinor ? [{ op: '<', version: nextMajor(partial) }] : [{ op: '<', version: nextMinor(partial) }]
|
|
111
147
|
default: return null
|
|
112
148
|
}
|
|
113
149
|
}
|
|
114
150
|
|
|
115
|
-
/**
|
|
151
|
+
/** Split a `||` alternative into comparator chunks, allowing `>= 1.2.3`. */
|
|
152
|
+
function tokenizeComparators(text) {
|
|
153
|
+
const chunks = []
|
|
154
|
+
let rest = text
|
|
155
|
+
while (rest.trim() !== '') {
|
|
156
|
+
rest = rest.replace(/^\s+/, '')
|
|
157
|
+
const match = /^(>=|<=|>|<|=|\^|~)?\s*([^\s,]+)\s*/.exec(rest)
|
|
158
|
+
if (!match) return null
|
|
159
|
+
const op = match[1]
|
|
160
|
+
const versionText = match[2]
|
|
161
|
+
// An operator with no following version (`>=` alone) is malformed; the
|
|
162
|
+
// regex above would otherwise read the next operator as a version.
|
|
163
|
+
if (!op && OPERATORS.has(versionText)) return null
|
|
164
|
+
chunks.push({ op, versionText })
|
|
165
|
+
rest = rest.slice(match[0].length)
|
|
166
|
+
}
|
|
167
|
+
return chunks.length > 0 ? chunks : null
|
|
168
|
+
}
|
|
169
|
+
|
|
116
170
|
function expandAlternative(alternative) {
|
|
117
171
|
const text = alternative.trim()
|
|
118
|
-
|
|
119
|
-
|
|
172
|
+
// An empty alternative is npm's `*` (node-semver treats `1.2.3 ||` as
|
|
173
|
+
// `*`). The wholly-empty range is rejected by the callers below.
|
|
174
|
+
if (text === '' || text === '*' || text === 'x' || text === 'X') return []
|
|
175
|
+
const hyphen = /^(\S+)\s+-\s+(\S+)$/.exec(text)
|
|
120
176
|
if (hyphen) {
|
|
121
177
|
const left = parsePartial(hyphen[1])
|
|
122
178
|
const right = parsePartial(hyphen[2])
|
|
123
179
|
if (!left || !right) return null
|
|
124
|
-
const low =
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
if (
|
|
133
|
-
|
|
180
|
+
const low = fullVersion(left)
|
|
181
|
+
// The left side is a floor: a concrete version keeps its exact prerelease,
|
|
182
|
+
// while a partial version gets the same -0 sentinel npm uses.
|
|
183
|
+
const lowToken = left.prerelease.length > 0
|
|
184
|
+
? { op: '>=', version: low }
|
|
185
|
+
: { op: '>=', version: sentinel(low) }
|
|
186
|
+
if (right.minor === null) return [lowToken, { op: '<', version: nextMajor(right) }]
|
|
187
|
+
if (right.patch === null) return [lowToken, { op: '<', version: nextMinor(right) }]
|
|
188
|
+
if (right.prerelease.length === 0) {
|
|
189
|
+
return [lowToken, { op: '<', version: sentinel({ ...fullVersion(right), patch: fullVersion(right).patch + 1 }) }]
|
|
190
|
+
}
|
|
191
|
+
return [lowToken, { op: '<=', version: fullVersion(right) }]
|
|
134
192
|
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
function tokenizeAlternative(alternative) {
|
|
139
|
-
const expanded = expandAlternative(alternative)
|
|
140
|
-
if (expanded === null) return null
|
|
141
|
-
if (expanded === '*') return [{ op: '*', version: null }]
|
|
193
|
+
const chunks = tokenizeComparators(text)
|
|
194
|
+
if (!chunks) return null
|
|
142
195
|
const tokens = []
|
|
143
|
-
for (const
|
|
144
|
-
const
|
|
145
|
-
if (
|
|
146
|
-
|
|
147
|
-
const version = parseVersion(match[2])
|
|
148
|
-
if (!version) return null
|
|
149
|
-
tokens.push({ op, version })
|
|
196
|
+
for (const chunk of chunks) {
|
|
197
|
+
const expanded = comparatorsForToken(chunk.op, chunk.versionText)
|
|
198
|
+
if (expanded === null) return null
|
|
199
|
+
tokens.push(...expanded)
|
|
150
200
|
}
|
|
151
201
|
return tokens
|
|
152
202
|
}
|
|
@@ -160,8 +210,6 @@ function satisfiesToken(version, token) {
|
|
|
160
210
|
case '>=': return cmp >= 0
|
|
161
211
|
case '<': return cmp < 0
|
|
162
212
|
case '<=': return cmp <= 0
|
|
163
|
-
case '^': return cmp >= 0 && compareVersions(version, upperForCaret(token.version)) < 0
|
|
164
|
-
case '~': return cmp >= 0 && compareVersions(version, upperForTilde(token.version)) < 0
|
|
165
213
|
default: return false
|
|
166
214
|
}
|
|
167
215
|
}
|
|
@@ -175,10 +223,9 @@ export function satisfies(version, range) {
|
|
|
175
223
|
const parsed = parseVersion(version)
|
|
176
224
|
if (!parsed || typeof range !== 'string') return false
|
|
177
225
|
if (range.trim() === '') return false
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
if (!tokens || tokens.length === 0) continue
|
|
226
|
+
for (const alternative of range.split('||')) {
|
|
227
|
+
const tokens = expandAlternative(alternative)
|
|
228
|
+
if (!tokens) continue
|
|
182
229
|
if (tokens.every(token => satisfiesToken(parsed, token))) return true
|
|
183
230
|
}
|
|
184
231
|
return false
|
|
@@ -188,7 +235,7 @@ export function satisfies(version, range) {
|
|
|
188
235
|
export function validRange(range) {
|
|
189
236
|
if (typeof range !== 'string' || range.trim() === '') return false
|
|
190
237
|
for (const alternative of range.split('||')) {
|
|
191
|
-
if (!
|
|
238
|
+
if (!expandAlternative(alternative)) return false
|
|
192
239
|
}
|
|
193
240
|
return true
|
|
194
241
|
}
|
package/40-checker/src/util.mjs
CHANGED
|
@@ -56,21 +56,31 @@ const BUILD_FILE = /(?:^|\/)(?:build|esbuild|vite|rollup|webpack)\.config\.[cm]?
|
|
|
56
56
|
|
|
57
57
|
/**
|
|
58
58
|
* Collect source files without following symlinks. Deterministic order, bounded.
|
|
59
|
+
* Returns the files plus scan metadata so callers can report truncation instead
|
|
60
|
+
* of pretending the bounded scan saw the whole package.
|
|
59
61
|
* @param {string} root
|
|
60
62
|
* @param {{maxFiles?: number, maxBytes?: number}} [options]
|
|
61
63
|
*/
|
|
62
|
-
export function
|
|
64
|
+
export function scanSourceFiles(root, options = {}) {
|
|
63
65
|
const maxFiles = options.maxFiles ?? 400
|
|
64
66
|
const maxBytes = options.maxBytes ?? 2_000_000
|
|
65
|
-
const
|
|
67
|
+
const paths = []
|
|
66
68
|
let bytes = 0
|
|
69
|
+
let truncated = false
|
|
70
|
+
let skippedForBytes = 0
|
|
67
71
|
const walk = (directory) => {
|
|
68
|
-
if (
|
|
72
|
+
if (paths.length >= maxFiles || bytes >= maxBytes) {
|
|
73
|
+
truncated = true
|
|
74
|
+
return
|
|
75
|
+
}
|
|
69
76
|
let entries
|
|
70
77
|
try { entries = fs.readdirSync(directory, { withFileTypes: true }) } catch { return }
|
|
71
78
|
entries.sort((a, b) => a.name.localeCompare(b.name))
|
|
72
79
|
for (const entry of entries) {
|
|
73
|
-
if (
|
|
80
|
+
if (paths.length >= maxFiles || bytes >= maxBytes) {
|
|
81
|
+
truncated = true
|
|
82
|
+
return
|
|
83
|
+
}
|
|
74
84
|
const absolute = path.join(directory, entry.name)
|
|
75
85
|
if (entry.isDirectory()) {
|
|
76
86
|
if (SKIP_DIRECTORIES.has(entry.name)) continue
|
|
@@ -84,24 +94,57 @@ export function listSourceFiles(root, options = {}) {
|
|
|
84
94
|
if (BUILD_FILE.test(relative)) continue
|
|
85
95
|
let size = 0
|
|
86
96
|
try { size = fs.statSync(absolute).size } catch { continue }
|
|
87
|
-
if (bytes + size > maxBytes)
|
|
97
|
+
if (bytes + size > maxBytes) {
|
|
98
|
+
// A file that does not fit is skipped; later smaller files may still
|
|
99
|
+
// fit, but the scan as a whole is no longer exhaustive.
|
|
100
|
+
truncated = true
|
|
101
|
+
skippedForBytes += 1
|
|
102
|
+
continue
|
|
103
|
+
}
|
|
88
104
|
bytes += size
|
|
89
|
-
|
|
105
|
+
paths.push(absolute)
|
|
90
106
|
}
|
|
91
107
|
}
|
|
92
108
|
walk(root)
|
|
93
|
-
return
|
|
109
|
+
return {
|
|
110
|
+
paths,
|
|
111
|
+
stats: {
|
|
112
|
+
files: paths.length,
|
|
113
|
+
bytes,
|
|
114
|
+
maxFiles,
|
|
115
|
+
maxBytes,
|
|
116
|
+
truncated,
|
|
117
|
+
skippedForBytes,
|
|
118
|
+
},
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function listSourceFiles(root, options = {}) {
|
|
123
|
+
return scanSourceFiles(root, options).paths
|
|
94
124
|
}
|
|
95
125
|
|
|
96
126
|
/** Read all source files once, returning {file, relative, text} records. */
|
|
97
127
|
export function readSources(root, options) {
|
|
98
|
-
return
|
|
128
|
+
return scanSourceFiles(root, options).paths.map(file => ({
|
|
99
129
|
file,
|
|
100
130
|
relative: posixRelative(root, file),
|
|
101
131
|
text: readText(file),
|
|
102
132
|
}))
|
|
103
133
|
}
|
|
104
134
|
|
|
135
|
+
/** Like readSources(), but also returns the bounded-scan metadata. */
|
|
136
|
+
export function readSourcesWithStats(root, options) {
|
|
137
|
+
const { paths, stats } = scanSourceFiles(root, options)
|
|
138
|
+
return {
|
|
139
|
+
sources: paths.map(file => ({
|
|
140
|
+
file,
|
|
141
|
+
relative: posixRelative(root, file),
|
|
142
|
+
text: readText(file),
|
|
143
|
+
})),
|
|
144
|
+
scan: stats,
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
105
148
|
export function regexHits(sources, pattern) {
|
|
106
149
|
const hits = []
|
|
107
150
|
for (const source of sources) {
|
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,26 @@
|
|
|
1
|
+
## 1.4.0 — 2026-11-06
|
|
2
|
+
|
|
3
|
+
### Added
|
|
4
|
+
|
|
5
|
+
- 契约文件:`spec/schemas/conformance.schema.json`(dsh-conformance/1)、`spec/dsc-clauses.json`(14 父 + 60 子)、`spec/schemas/boost-card.schema.json` + 示例、`spec/examples/flow-evidence.example.json`、`spec/coverage-contract.md`。
|
|
6
|
+
- `notChecked[]`:NA/ERROR/partial 项带 `reason` + `howToEnable`,报告能回答“为什么没查、怎么才能查”。
|
|
7
|
+
- bundle/client 的 DSC-04–09 静态子集(PASS/WARN + partial),不再 8 项全 NA。
|
|
8
|
+
- sidecar 加 `title/repo/license/scope/checks/checkedBy/notChecked`,种 20 条真实插件。
|
|
9
|
+
- `check` 默认 `--entry-claim static --no-exec`,报告标注“未执行插件代码”;host 版本三态(显式/环境/受控探测),默认不执行 PATH 的 dsh。
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- C-2 range 解析:支持 `>= 0.2.2-rc.1`、hyphen/x-range、`||`,prerelease 语义对齐 node-semver;合法范围不再误报 C-2.1,覆盖判断归 C-2.2。
|
|
14
|
+
- DSC-01 检查全部 `exports` 子路径与 `main` 的真实文件存在性,坏入口 FAIL。
|
|
15
|
+
- DSC-10 文件截断改为 `NA + coverage=partial` 并明示截断位置,不再假报 ERROR。
|
|
16
|
+
- advice:54 条规则 docsUrl 改到具体小节;C-11.4/C-12.4 同证据合并;`package.json#field` 补 `file:line`。
|
|
17
|
+
- 文档纠偏:删除 BOOST 的 -17%/-33%/-25%、「保证更省」、「token 直接计价」,明确「BOOST 与 FLOW 不可比、性能未证实」。
|
|
18
|
+
- README/SUPPORTED 对齐 1.4.0,补 Windows 行与 14 项默认检查说明。
|
|
19
|
+
|
|
20
|
+
### Verified
|
|
21
|
+
|
|
22
|
+
- `npm test` 232/232。
|
|
23
|
+
|
|
1
24
|
## 1.3.0 — 2026-11-06
|
|
2
25
|
|
|
3
26
|
### Added
|
package/README.md
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
|
|
24
24
|
```sh
|
|
25
25
|
npm i -g dsh-standards # 全局:dsh-standards + dsh-levels
|
|
26
|
-
npx dsh-standards <plugin-dir>
|
|
26
|
+
npx dsh-standards check <plugin-dir> # 安全默认:静态检查,不执行插件代码
|
|
27
27
|
```
|
|
28
28
|
|
|
29
29
|
</div>
|
|
@@ -44,9 +44,9 @@ DSH 插件的采用深度分三层,每层一个问题、一套证据状态,*
|
|
|
44
44
|
|
|
45
45
|
仓里就三样东西:`40-checker/`(DS-C 一致性检查器与全部证据脚本)、`bin/`(两个公开入口 `dsh-standards` 与 `dsh-levels`),以及 `spec/` 与 `31`–`35` 号规范正文。工具只读目标包,不改被检对象;结论只认证据,`not-run`、`NA`、`static` 都不会被写成 PASS。
|
|
46
46
|
|
|
47
|
-
npm 上的旧占位 `dsh-standards@0.0.3` 只是名字预留,这条 1.x 线取代了它(当前 `1.
|
|
47
|
+
npm 上的旧占位 `dsh-standards@0.0.3` 只是名字预留,这条 1.x 线取代了它(当前 `1.3.0`);两个 bin 分别叫 `dsh-standards` 与 `dsh-levels`。
|
|
48
48
|
|
|
49
|
-
|
|
49
|
+
锚点不是硬编码的通行证:`repo` / `commit` / `cordis` 记在报告里,`dsh` 由 `--host-version`、`DSH_HOST_VERSION` 或显式 `--allow-host-probe` 解析;都没有时报告写「host 版本未知」,并标 `source=default, suspect=true`。换一次版本就是另一把尺子,旧报告不能直接当新版本的证据。
|
|
50
50
|
|
|
51
51
|
### 当前硬数据
|
|
52
52
|
|
|
@@ -55,7 +55,7 @@ npm 上的旧占位 `dsh-standards@0.0.3` 只是名字预留,这条 1.x 线取
|
|
|
55
55
|
- 18 包 base-only runtime:**18/18 有原始 JSON;10/18 G3 verified,8 partial,0 error**;host gate 后 **8 clean + 2 reject + 8 partial**;
|
|
56
56
|
- client runtime:**15/15 `client-module-loaded`**(真实 Chromium);web-app **4/4 loaded**;
|
|
57
57
|
- 不可用:**0 plugin code FAIL;2 host-gate reject(telemetry/verification)**;
|
|
58
|
-
-
|
|
58
|
+
- 检查器自测:**`npm test` 全绿**(本地 Node 24;含安全默认、host 三态、治理、DCO、评价记录、声明、验真、链、漂移测试);
|
|
59
59
|
- J1:**6 PASS + 1 WARN**;LEV-3:volatile **0/20** vs control **20/20** remount;LEV-6:稳定 prefix **100%** vs 易变 **0%**;LEV-7:**8.24×**(合成,非生产基线)。
|
|
60
60
|
|
|
61
61
|
这些数字是自评与仓内证据链的输出,不是第三方复核结论——报告里的 `notClaimed` 字段会把「官方标准、认证、安全审计、排名、生产基线」逐条列出来。
|
|
@@ -72,6 +72,7 @@ npm 上的旧占位 `dsh-standards@0.0.3` 只是名字预留,这条 1.x 线取
|
|
|
72
72
|
- [33-DSH-BOOST 加速实践](33-DSH-BOOST加速实践.md) — 8 条 LEV
|
|
73
73
|
- [34-三层分级模型(GB/GB-T/GB-Z 类比)](34-三层分级模型-GB-GBT-GBZ类比.md) — 为什么是三层、怎么读
|
|
74
74
|
- [35-三层开发建议与验收清单](35-三层开发建议与验收清单.md) — 作者最短路径 / 评审清单
|
|
75
|
+
- [docs/coverage.md](docs/coverage.md) — 14 项检查:默认查什么、不查什么、怎么开启
|
|
75
76
|
- [36-插件全生命周期与工具链](36-插件全生命周期与工具链.md) — 从脚手架到退役
|
|
76
77
|
- [37-PerryLink 插件生态三层对照](37-PerryLink插件生态-三层对照与升级清单.md) — 我的仓怎么升级
|
|
77
78
|
- [38-三层标准 RFC](38-RFC-三层标准-待商议问题.md) — 内测群商议的 8 个问题
|
|
@@ -116,6 +117,9 @@ npx -p dsh-standards dsh-levels --help
|
|
|
116
117
|
**2. 跑一次检查**(目标是一个含 `package.json` 的包目录):
|
|
117
118
|
|
|
118
119
|
```sh
|
|
120
|
+
# 作者入口(推荐):默认 --entry-claim static --no-exec,不执行插件代码
|
|
121
|
+
dsh-standards check ./my-plugin
|
|
122
|
+
|
|
119
123
|
# 入门兼容性:静态识别,不声明运行时证据
|
|
120
124
|
dsh-standards ./my-plugin --scope entry --entry-claim static
|
|
121
125
|
|
|
@@ -127,6 +131,8 @@ dsh-standards ./my-plugin --format sarif --out report.sarif
|
|
|
127
131
|
dsh-levels ./my-plugin --format json --out audit.json
|
|
128
132
|
```
|
|
129
133
|
|
|
134
|
+
`check` 是面向作者的安全入口:**默认 `--entry-claim static --no-exec`,不执行插件代码**;Markdown 报告顶部会写明「本次未执行插件代码」。要跑 L1–L3,必须显式给 `--runtime <cordis lib/index.js>`(bundle 用 `--host`)或 `--entry-claim runtime`,否则运行时项一律记 `not-run`,绝不写成 PASS。它也不会无条件执行 PATH 里的 `dsh`:anchor 依次取 `--host-version`、`$DSH_HOST_VERSION`,再次才是显式 `--allow-host-probe`;三者都没有时报告写「host 版本未知」,并标 `source=default, suspect=true`。
|
|
135
|
+
|
|
130
136
|
**3. 看结论**——先看 exit code,再看 `status`:
|
|
131
137
|
|
|
132
138
|
```text
|
|
@@ -191,19 +197,21 @@ coverage: mandatory=5 pass=5 warn=0 fail=0 na=0 notRun=0
|
|
|
191
197
|
|
|
192
198
|
## 配置
|
|
193
199
|
|
|
194
|
-
|
|
200
|
+
没有配置文件——全部可调项就是命令行参数。三个入口各一套:`dsh-standards check <package-dir>` 是作者安全入口(默认静态、不执行插件代码),`dsh-standards <package-dir>` 是向后兼容的裸检查器(不附加安全默认,`--help` 打印完整用法),`dsh-levels <package-dir>` 转发给三层审计。下表默认值指 `check`;裸检查器的 `--entry-claim` / `--no-exec` 默认仍是 `runtime` / `false`。
|
|
195
201
|
|
|
196
|
-
最常用的
|
|
202
|
+
最常用的 10 个:
|
|
197
203
|
|
|
198
204
|
| 参数 | 默认值 | 含义 |
|
|
199
205
|
|---|---|---|
|
|
200
206
|
| `<package-dir>` | `.` | 目标包目录,必须含 `package.json`;tarball 不在支持范围 |
|
|
201
207
|
| `--scope` | `ds-c` | `mvp` = 8 个 MVP ID;`ds-c` = 全量声明;`entry` = 兼容性门 |
|
|
202
208
|
| `--format` | `text` | `text` / `json` / `sarif`(SARIF 2.1.0) |
|
|
203
|
-
| `--entry-claim` | `runtime
|
|
204
|
-
| `--host` | — | app-boot 的 `lib/index.js`;bundle 的 G0–G3
|
|
209
|
+
| `--entry-claim` | `static`(`check`;裸检查器 `runtime`) | `runtime` 需 DSC-14 运行时证据;`static` 只声明结构识别 |
|
|
210
|
+
| `--host` | — | app-boot 的 `lib/index.js`;bundle 的 G0–G3 冒烟必需,也视为执行 opt-in |
|
|
205
211
|
| `--profile` | `package` | `package` / `listing` / `both`;给了 `--market-index` 时默认 `listing` |
|
|
206
|
-
| `--no-exec` | `false
|
|
212
|
+
| `--no-exec` | `true`(`check`;裸检查器 `false`) | 跳过 L1–L3,记 `not-run`(绝不记 PASS) |
|
|
213
|
+
| `--host-version` | `$DSH_HOST_VERSION` | `check` 专用的宿主 dsh 版本;不执行任何 `dsh` |
|
|
214
|
+
| `--allow-host-probe` | `false` | 显式允许 `check` 解析 `dsh` 的 JS 入口并 `node ... --version` 探测 |
|
|
207
215
|
| `--out` | — | 报告写入文件;不给就打 stdout |
|
|
208
216
|
|
|
209
217
|
<details>
|
|
@@ -215,19 +223,21 @@ coverage: mandatory=5 pass=5 warn=0 fail=0 na=0 notRun=0
|
|
|
215
223
|
| `--scope` | `ds-c` | 检查范围:`mvp` / `ds-c` / `entry` |
|
|
216
224
|
| `--profile` | `package` | 评估 profile:`package` / `listing` / `both` |
|
|
217
225
|
| `--runtime` | — | 指向构建好的 `@deepseek-ai/cordis` `lib/index.js`,开启 L1–L3 |
|
|
218
|
-
| `--runtime-version` |
|
|
226
|
+
| `--runtime-version` | 宿主版本(`check`);否则内置常量 | 写进 anchor 的 dsh 运行时版本,供 C-2 判定;报告用 `anchor.runtimeSource` 说明来源 |
|
|
219
227
|
| `--cordis-version` | `4.0.6-alpha.1` | 写进 anchor 的 cordis 版本 |
|
|
220
228
|
| `--commit` | `ce7073d51254` | 写进 anchor 的 harness commit |
|
|
221
229
|
| `--install-spec` | — | 给 C-3 用的推荐安装 spec |
|
|
222
230
|
| `--market-index` | — | 格式 1 的市场索引 JSON,开启 listing 模式 |
|
|
223
231
|
| `--plugin-name` | — | 市场索引里的插件名 |
|
|
224
232
|
| `--timeout` | `10000` | L1–L3 超时毫秒(100–120000) |
|
|
225
|
-
| `--no-exec` | `false` | 完全跳过 L1–L3,记 `not-run` / `NA` |
|
|
233
|
+
| `--no-exec` | `false`(裸检查器;`check` 默认 `true`) | 完全跳过 L1–L3,记 `not-run` / `NA` |
|
|
226
234
|
| `--static-only` | `false` | M1 模式:只留 C-1/C-2/C-3/C-11/C-12/C-13,C-4–C-9 直接出界 |
|
|
227
235
|
| `--treat-as-plugin` | `false` | 对 bundle 的 server 入口跑 L1–L3 |
|
|
228
236
|
| `--entry-runtime` | `require` | entry 门要完整 G0–G3,还是接受 `server-entry` 代理 |
|
|
229
|
-
| `--entry-claim` | `runtime` | entry 门要运行时证据(DSC-14),还是只做静态识别 |
|
|
230
|
-
| `--host` | — | app-boot `lib/index.js`,开启 bundle 的 G0–G3
|
|
237
|
+
| `--entry-claim` | `runtime`(裸检查器;`check` 默认 `static`) | entry 门要运行时证据(DSC-14),还是只做静态识别 |
|
|
238
|
+
| `--host` | — | app-boot `lib/index.js`,开启 bundle 的 G0–G3 冒烟(`check` 中视为执行 opt-in) |
|
|
239
|
+
| `--host-version` | `$DSH_HOST_VERSION` | `check` 专用:直接声明宿主版本,不执行 `dsh` |
|
|
240
|
+
| `--allow-host-probe` | `false` | `check` 专用:显式允许探测宿主版本 |
|
|
231
241
|
| `--base` | — | bundle 冒烟用的 base `cordis.patch.yml` |
|
|
232
242
|
| `--web` | — | bundle 冒烟用的 web-app `cordis.patch.yml` |
|
|
233
243
|
| `--bare` | — | host farm 的 bare-module 基线目录 |
|
|
@@ -297,7 +307,8 @@ coverage: mandatory=5 pass=5 warn=0 fail=0 na=0 notRun=0
|
|
|
297
307
|
- **不声称官方推荐或采纳;不声称认证 / 认可 / 安全审计。**
|
|
298
308
|
- **MVP 覆盖缺口**:`--scope mvp` 只落 8 个 ID,`DSC-03/10/11/12/13` 不在其中;它的结论是 `subset: "MVP-13"`、`level: null`,不得当 DS-C 徽章用。
|
|
299
309
|
- **只吃包目录**:目标必须是含 `package.json` 的目录;tarball 支持不在 MVP 里。
|
|
300
|
-
- **运行时结论有前置条件**:没有 `--runtime` 或 `--host` 时 L1–L3 只能记 `not-run`;`--entry-claim static`
|
|
310
|
+
- **运行时结论有前置条件**:没有 `--runtime` 或 `--host` 时 L1–L3 只能记 `not-run`;`--entry-claim static` 永远给不出运行时结论。`check` 默认就是不执行(`--no-exec`),这是刻意的安全默认,不是缺陷。
|
|
311
|
+
- **host 版本可能未知**:`check` 不无条件执行 PATH 里的 `dsh`;没有 `--host-version` / `DSH_HOST_VERSION` / `--allow-host-probe` 时,anchor 写默认常量并标 `source=default, suspect=true`,报告顶部写「host 版本未知」。
|
|
301
312
|
- **结论带版本锚点**:`commit` / `dsh` / `cordis` 变了就是另一把尺子,旧报告不能直接当新版本的证据。
|
|
302
313
|
- **BOOST 数据不是生产基线**:LEV 的 8/8 measured 来自 loader / context / provider-capture / synthetic timer 的合成环境,LEV-7 的 8.24× 同理。
|
|
303
314
|
- **文本改动必须说明**:复制或修改本仓文本时,必须在显著位置给出 Change Notice(来源 commit、改了哪个章节、改了什么、为什么、日期),见 [`MODIFICATION-POLICY.md`](MODIFICATION-POLICY.md);标志与 badge 不随许可证授权,规则见 `governance/mark-policy.md`。
|
|
@@ -308,9 +319,10 @@ coverage: mandatory=5 pass=5 warn=0 fail=0 na=0 notRun=0
|
|
|
308
319
|
git clone https://github.com/PerryLink/dsh-standards.git
|
|
309
320
|
cd dsh-standards
|
|
310
321
|
|
|
311
|
-
node --test 40-checker/test/*.test.mjs #
|
|
322
|
+
node --test 40-checker/test/*.test.mjs # 全绿(含安全默认与 host 三态)
|
|
323
|
+
node bin/dsh-standards.mjs check 40-checker/fixtures/good # 安全默认:静态、不执行
|
|
312
324
|
node bin/dsh-standards.mjs 40-checker/fixtures/good \
|
|
313
|
-
--scope entry --entry-claim static --format text #
|
|
325
|
+
--scope entry --entry-claim static --format text # 裸检查器 fixture 冒烟
|
|
314
326
|
node bin/dsh-levels.mjs 40-checker/fixtures/good --format json
|
|
315
327
|
|
|
316
328
|
# 从源码直连检查器:结构识别 / 声明运行(bundle 需要 host)
|
package/docs/coverage.md
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# 覆盖说明(14 项)— `dsh-standards check` 默认查什么
|
|
2
|
+
|
|
3
|
+
`dsh-standards check <package-dir>` 是作者入口。**默认 `--entry-claim static --no-exec`:不执行插件代码**。本文只描述这套默认,不把 `not-run` / `NA` 写成 PASS。
|
|
4
|
+
|
|
5
|
+
默认命令等价于:
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
dsh-standards check <package-dir> \
|
|
9
|
+
--entry-claim static --no-exec --scope ds-c --profile package
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## 一眼看总数
|
|
13
|
+
|
|
14
|
+
| 口径 | 数量 | 说明 |
|
|
15
|
+
|---|---|---|
|
|
16
|
+
| 报告固定携带 | **14** | `DSC-01` … `DSC-14` |
|
|
17
|
+
| 默认实际评估 | **6** | `DSC-01`、`DSC-02`、`DSC-10`、`DSC-11`、`DSC-12`、`DSC-13` |
|
|
18
|
+
| 默认未检查 | **8** | `DSC-03`(NA)+ `DSC-04`–`DSC-09`(not-run)+ `DSC-14`(NA) |
|
|
19
|
+
|
|
20
|
+
`not checked` 桶把 NA / not-run / ERROR 都列出来;**没检查不等于通过**。`DSC-10` 是 advisory,只提供信号,不改变结论。
|
|
21
|
+
|
|
22
|
+
## 14 项逐条
|
|
23
|
+
|
|
24
|
+
| ID | 层 | 默认状态 | 默认为什么 | 怎么开启 |
|
|
25
|
+
|---|---|---|---|---|
|
|
26
|
+
| `DSC-01` | L0 | **检查** | manifest / name / version / license / 入口 / 导出形态 | 默认即查 |
|
|
27
|
+
| `DSC-02` | L0 | **检查** | `engines.dsh` / peer 是否被 anchor 的宿主版本满足 | 默认即查;版本源见下 |
|
|
28
|
+
| `DSC-03` | L0 | **NA** | 没有安装 spec / 市场索引可查 | `--install-spec <spec>`,或 `--market-index <file> --plugin-name <name>`(listing) |
|
|
29
|
+
| `DSC-04` | L1 | **not-run** | `--no-exec`,不导入插件 | `--runtime <cordis lib/index.js>`;bundle 用 `--host <app-boot lib/index.js>` |
|
|
30
|
+
| `DSC-05` | L1 | **not-run** | 同上 | 同上 |
|
|
31
|
+
| `DSC-06` | L1+L2 | **not-run** | 同上 | 同上 |
|
|
32
|
+
| `DSC-07` | L2 | **not-run** | 同上 | 同上 |
|
|
33
|
+
| `DSC-08` | L3 | **not-run** | 同上 | 同上 |
|
|
34
|
+
| `DSC-09` | L1 | **not-run** | 同上 | 同上 |
|
|
35
|
+
| `DSC-10` | L0 | **检查(advisory)** | import 期副作用静态信号 | 默认即查;不单独 FAIL |
|
|
36
|
+
| `DSC-11` | L0 | **检查** | `dsh.capabilities` 与高信号触发 | 默认即查 |
|
|
37
|
+
| `DSC-12` | L0 | **检查** | 凭据 backing file、`listRecords`、seam、commitments/disclosure 形状 | 默认即查 |
|
|
38
|
+
| `DSC-13` | L0 | **检查** | `dsh.labels` / `upstream` / 禁用语标签 | 默认即查 |
|
|
39
|
+
| `DSC-14` | L1 | **NA** | 默认 `entry-claim=static`,不声明运行时证据 | `--scope entry --entry-claim runtime --runtime <...>`(bundle 加 `--host`) |
|
|
40
|
+
|
|
41
|
+
## 执行 opt-in
|
|
42
|
+
|
|
43
|
+
默认不执行插件代码。以下任一参数出现,才算显式允许运行时检查:
|
|
44
|
+
|
|
45
|
+
- `--runtime <path>`:Cordis 运行时的 `lib/index.js`,开启 L1–L3;
|
|
46
|
+
- `--host <file>`:app-boot 入口,开启 bundle 的 G0–G3 冒烟;
|
|
47
|
+
- `--entry-claim runtime`:entry 门声明运行时证据(通常还要配 `--runtime` / `--host`)。
|
|
48
|
+
|
|
49
|
+
想保留运行时声明但只看静态结果,显式加 `--no-exec`。`--runtime-version` 只是 anchor,**不是**执行开关。
|
|
50
|
+
|
|
51
|
+
## host 版本从哪里来
|
|
52
|
+
|
|
53
|
+
`check` 不会无条件执行 PATH 里的 `dsh`。anchor 按以下顺序取:
|
|
54
|
+
|
|
55
|
+
1. `--host-version <v>`;
|
|
56
|
+
2. `DSH_HOST_VERSION` 环境变量;
|
|
57
|
+
3. `--allow-host-probe`:解析 `dsh` 的 JS 入口,执行 `node <entry> --version`(仅此一种情况会探测);
|
|
58
|
+
4. 都没有:报告写「host 版本未知」,`anchor.runtimeSource=default`、`anchor.suspect=true`。
|
|
59
|
+
|
|
60
|
+
三态在 JSON 里可直接核对:`anchor.hostVersionKnown`、`anchor.hostVersionSource`(`flag` / `env` / `probe` / `none`)、`anchor.runtimeSource`(`host` / `default`;显式 `--runtime-version` 时保留 `cli`)。
|
|
61
|
+
|
|
62
|
+
## 跨平台
|
|
63
|
+
|
|
64
|
+
- **Windows:** `unshare -Urn` / `prlimit` 不可用 → sandbox degraded;本仓**不声称** Windows 上的 runtime 结论,矩阵里的 Windows / Node 22 行固定 `not-run`。静态检查(上表 6 项)仍可跑。
|
|
65
|
+
- **WSL2(Ubuntu 24.04):** Linux 沙箱可用,能提供 `unshare` / `prlimit`;这是本仓做 runtime 采集的环境。WSL2 可用不等于某个 host × Node × checker 组合已经跑过,具体以 [SUPPORTED.md](../SUPPORTED.md) 的格子为准。
|
|
66
|
+
- **Node:** 包声明 `engines.node >=24.9.0`;Node 22 只在自测 / 静态矩阵中有记录(见 SUPPORTED.md)。
|
|
67
|
+
|
|
68
|
+
## 复核命令
|
|
69
|
+
|
|
70
|
+
```sh
|
|
71
|
+
# 默认:6/14 评估,8 项 not-run/NA
|
|
72
|
+
dsh-standards check <package-dir> --json | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{const r=JSON.parse(s);console.log(r.coverage.checked+"/"+r.coverage.total);console.log(r.anchor);console.log(r.safety)})'
|
|
73
|
+
|
|
74
|
+
# 显式声明宿主版本,仍不执行插件
|
|
75
|
+
dsh-standards check <package-dir> --host-version 0.2.2-rc.1
|
|
76
|
+
|
|
77
|
+
# 显式允许宿主版本探测(只跑 dsh --version,不跑插件)
|
|
78
|
+
dsh-standards check <package-dir> --allow-host-probe
|
|
79
|
+
|
|
80
|
+
# 显式开启 L1–L3(会执行插件代码)
|
|
81
|
+
dsh-standards check <package-dir> --runtime /path/to/cordis/lib/index.js
|
|
82
|
+
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-standards",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "DSH plugin standards and conformance checks: ENTRY/FLOW checks plus BOOST guidance. Non-official; no certification or ranking.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "40-checker/src/index.mjs",
|