dsh-standards 1.3.0 → 1.4.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.
@@ -1,17 +1,23 @@
1
1
  /**
2
2
  * Minimal SemVer range matcher for the checker.
3
3
  *
4
- * It is intentionally small: the host evaluates plugin requirements with
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:52).
7
- * This module implements the subset that appears in package manifests:
8
- * exact versions, `>= > <= <`, `^`, `~`, `*`/`x`, space-separated AND,
9
- * and `||` alternatives. It does NOT implement hyphen ranges, `!=`,
10
- * or npm's "prerelease only matches same tuple" exclusion; the host uses
11
- * includePrerelease, so we include prereleases everywhere.
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, prerelease: [] }
58
- if (version.minor > 0) return { ...version, minor: version.minor + 1, patch: 0, prerelease: [] }
59
- return { ...version, patch: version.patch + 1, prerelease: [] }
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, prerelease: [] }
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
- function expandToken(raw) {
88
- if (raw === '*' || raw === 'x' || raw === 'X') return '*'
89
- const match = /^(>=|<=|>|<|=|\^|~)?\s*v?(\d+)(?:\.(\d+|[xX*]))?(?:\.(\d+|[xX*]))?(?:-([0-9A-Za-z.-]+))?$/.exec(raw)
90
- if (!match) return null
91
- const op = match[1] ?? '='
92
- const partial = {
93
- major: Number(match[2]),
94
- minor: match[3] === undefined || /[xX*]/.test(match[3]) ? null : Number(match[3]),
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
- if (!noMinor && !noPatch) return `${op}${fmt(low)}`
102
- switch (op) {
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 ? `>=${fmt(low)} <${fmt(nextMajor(partial))}` : `>=${fmt(low)} <${fmt(nextMinor(partial))}`
107
- case '>=': return `>=${fmt(low)}`
108
- case '>': return noMinor ? `>=${fmt(nextMajor(partial))}` : `>=${fmt(nextMinor(partial))}`
109
- case '<': return `<${fmt(low)}`
110
- case '<=': return noMinor ? `<${fmt(nextMajor(partial))}` : `<${fmt(nextMinor(partial))}`
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
- /** Hyphen ranges and x-ranges are normalized before tokenizing. */
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
- if (text === '' || text === '*' || text === 'x' || text === 'X') return '*'
119
- const hyphen = /^(.+?)\s+-\s+(.+)$/.exec(text)
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 = `>=${fmt(fullVersion(left))}`
125
- if (right.minor === null) return `${low} <${fmt(nextMajor(right))}`
126
- if (right.patch === null) return `${low} <${fmt(nextMinor(right))}`
127
- return `${low} <=${fmt(fullVersion(right))}`
128
- }
129
- const parts = []
130
- for (const raw of text.split(/\s+/)) {
131
- const expanded = expandToken(raw)
132
- if (expanded === null) return null
133
- parts.push(expanded)
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
- return parts.join(' ')
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 raw of expanded.split(/\s+/)) {
144
- const match = /^(>=|<=|>|<|=|\^|~)?\s*v?(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?)$/.exec(raw)
145
- if (!match) return null
146
- const op = match[1] ?? '='
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 alternatives = range.split('||')
179
- for (const alternative of alternatives) {
180
- const tokens = tokenizeAlternative(alternative)
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 (!tokenizeAlternative(alternative)) return false
238
+ if (!expandAlternative(alternative)) return false
192
239
  }
193
240
  return true
194
241
  }
@@ -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 listSourceFiles(root, options = {}) {
64
+ export function scanSourceFiles(root, options = {}) {
63
65
  const maxFiles = options.maxFiles ?? 400
64
66
  const maxBytes = options.maxBytes ?? 2_000_000
65
- const files = []
67
+ const paths = []
66
68
  let bytes = 0
69
+ let truncated = false
70
+ let skippedForBytes = 0
67
71
  const walk = (directory) => {
68
- if (files.length >= maxFiles || bytes >= maxBytes) return
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 (files.length >= maxFiles || bytes >= maxBytes) return
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) continue
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
- files.push(absolute)
105
+ paths.push(absolute)
90
106
  }
91
107
  }
92
108
  walk(root)
93
- return files
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 listSourceFiles(root, options).map(file => ({
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,30 @@
1
+ ## 1.4.1 — 2026-11-06
2
+
3
+ - 建议的 `docsUrl` 基址跟到 `dsh-plugin-guide@v0.4.6`(FLOW 示例修正版),不再指向 0.4.4。
4
+
5
+ ## 1.4.0 — 2026-11-06
6
+
7
+ ### Added
8
+
9
+ - 契约文件:`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`。
10
+ - `notChecked[]`:NA/ERROR/partial 项带 `reason` + `howToEnable`,报告能回答“为什么没查、怎么才能查”。
11
+ - bundle/client 的 DSC-04–09 静态子集(PASS/WARN + partial),不再 8 项全 NA。
12
+ - sidecar 加 `title/repo/license/scope/checks/checkedBy/notChecked`,种 20 条真实插件。
13
+ - `check` 默认 `--entry-claim static --no-exec`,报告标注“未执行插件代码”;host 版本三态(显式/环境/受控探测),默认不执行 PATH 的 dsh。
14
+
15
+ ### Fixed
16
+
17
+ - C-2 range 解析:支持 `>= 0.2.2-rc.1`、hyphen/x-range、`||`,prerelease 语义对齐 node-semver;合法范围不再误报 C-2.1,覆盖判断归 C-2.2。
18
+ - DSC-01 检查全部 `exports` 子路径与 `main` 的真实文件存在性,坏入口 FAIL。
19
+ - DSC-10 文件截断改为 `NA + coverage=partial` 并明示截断位置,不再假报 ERROR。
20
+ - advice:54 条规则 docsUrl 改到具体小节;C-11.4/C-12.4 同证据合并;`package.json#field` 补 `file:line`。
21
+ - 文档纠偏:删除 BOOST 的 -17%/-33%/-25%、「保证更省」、「token 直接计价」,明确「BOOST 与 FLOW 不可比、性能未证实」。
22
+ - README/SUPPORTED 对齐 1.4.0,补 Windows 行与 14 项默认检查说明。
23
+
24
+ ### Verified
25
+
26
+ - `npm test` 232/232。
27
+
1
28
  ## 1.3.0 — 2026-11-06
2
29
 
3
30
  ### 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> --scope entry --entry-claim static # 或不装,直接用 npx
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.1.2`);两个 bin 分别叫 `dsh-standards` 与 `dsh-levels`。
47
+ npm 上的旧占位 `dsh-standards@0.0.3` 只是名字预留,这条 1.x 线取代了它(当前 `1.3.0`);两个 bin 分别叫 `dsh-standards` 与 `dsh-levels`。
48
48
 
49
- 固定锚点(写在 `spec/levels.json` 与报告里):`repo=deepseek-ai/deepseek-harness`、`commit=ce7073d51254`、`dsh=0.2.2-alpha.2`、`cordis=4.0.6-alpha.1`。换运行时版本要显式传参,报告会记下你量的是哪一版。
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
- - 检查器自测:**128/128 tests**(本地 `node --test 40-checker/test/*.test.mjs`;含治理、DCO、评价记录、声明、验真、链、漂移测试);
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
- 没有配置文件——全部可调项就是命令行参数。两个命令各一套:`dsh-standards <package-dir>` 转发给 DS-C 检查器(`--help` 打印完整用法),`dsh-levels <package-dir>` 转发给三层审计。
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
- 最常用的 8 个:
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` | `runtime` 需 DSC-14 运行时证据;`static` 只声明结构识别 |
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` | 跳过 L1–L3,记 `not-run`(绝不记 PASS) |
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` | `0.2.2-alpha.2` | 写进 anchor 的 dsh 运行时版本,供 C-2 判定 |
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 # 128/128
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 # 本仓 fixture 冒烟
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)
@@ -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.0",
3
+ "version": "1.4.1",
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",