dsh-standards 1.2.2 → 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.
Files changed (46) hide show
  1. package/40-checker/README.md +18 -1
  2. package/40-checker/scripts/build-offline-kit.mjs +284 -0
  3. package/40-checker/scripts/build-sidecar.mjs +714 -0
  4. package/40-checker/scripts/flow-collect.mjs +7 -1
  5. package/40-checker/scripts/level-audit.mjs +45 -7
  6. package/40-checker/scripts/run-tests.mjs +3 -7
  7. package/40-checker/src/author-cli.mjs +406 -0
  8. package/40-checker/src/author-report.mjs +271 -0
  9. package/40-checker/src/bundle-static.mjs +181 -0
  10. package/40-checker/src/cli.mjs +100 -11
  11. package/40-checker/src/client-shape.mjs +33 -3
  12. package/40-checker/src/declarations.mjs +53 -8
  13. package/40-checker/src/flow-evidence.mjs +106 -1
  14. package/40-checker/src/index.mjs +6 -4
  15. package/40-checker/src/manifest.mjs +87 -14
  16. package/40-checker/src/remediation.mjs +108 -7
  17. package/40-checker/src/report.mjs +203 -26
  18. package/40-checker/src/semver.mjs +112 -65
  19. package/40-checker/src/util.mjs +51 -8
  20. package/CHANGELOG.md +44 -0
  21. package/README.en.md +7 -2
  22. package/README.es.md +7 -2
  23. package/README.hi.md +7 -2
  24. package/README.md +35 -18
  25. package/README.pt.md +7 -2
  26. package/bin/dsh-standards.mjs +13 -2
  27. package/docs/assets/dsh-standards-flow-v3-dark.svg +87 -0
  28. package/docs/assets/dsh-standards-flow-v3-light.svg +87 -0
  29. package/docs/assets/dsh-standards-flow-v4-dark.svg +79 -0
  30. package/docs/assets/dsh-standards-flow-v4-light.svg +79 -0
  31. package/docs/assets/dsh-standards-hero-en-dark.webp +0 -0
  32. package/docs/assets/dsh-standards-hero-en-light.webp +0 -0
  33. package/docs/assets/dsh-standards-hero-zh-dark.webp +0 -0
  34. package/docs/assets/dsh-standards-hero-zh-light.webp +0 -0
  35. package/docs/coverage.md +82 -0
  36. package/package.json +1 -1
  37. package/spec/README.md +2 -0
  38. package/spec/coverage-contract.md +100 -0
  39. package/spec/dsc-clauses.json +766 -0
  40. package/spec/examples/boost-card.example.json +15 -0
  41. package/spec/examples/flow-evidence.example.json +113 -0
  42. package/spec/flow-evidence.schema.json +6 -2
  43. package/spec/remediation.json +54 -54
  44. package/spec/schemas/advice.schema.json +92 -0
  45. package/spec/schemas/boost-card.schema.json +107 -0
  46. 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
- * 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,47 @@
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
+
24
+ ## 1.3.0 — 2026-11-06
25
+
26
+ ### Added
27
+
28
+ - `check [dir]` 子命令:默认 cwd、Markdown 报告(一句结论、≤3 条优先修复、分桶全表、advice、免责声明),`--json` 与 `--html`(单文件、内联 CSS、零依赖)。
29
+ - `spec/schemas/advice.schema.json`(`dsh-advice/1`),advice 逐条过 schema。
30
+ - 证据主体绑定:`--flow-evidence --dir` 校验 name/version/treeSha256/tarball 哈希,不一致即 `invalid`。
31
+ - CI 归属:collector 写 `producer.runUrl/repository/commit`,audit 标 `attestation: self-declared|ci`(不升级 claimLevel)。
32
+ - sidecar 生成(`publish/sidecar.json`,`dsh-market-sidecar/1`)、每日过期/撤回 workflow、离线包(~580 KB)。
33
+ - `SUPPORTED.md`(host × node × checker 矩阵 + 证据锚)与 `CHANGELOG-RULES.md`(规则级变更与影响面)。
34
+
35
+ ### Fixed
36
+
37
+ - coverage 按全部 14 项统计,`na/notRun/error` 不再为 0;未查 C-3.1/3.2/3.5 时 `entryBlocking.pass=false`,并输出 `checked/notChecked`。
38
+ - 默认 runtime 锚改为宿主实际版本(`anchor.runtimeSource=host`),与 `--runtime-version` 不一致时 `anchor.suspect=true`;不再固定 `0.2.2-alpha.2`。
39
+ - 报告分三桶:`blocking`/`hygiene`(DSC-11/12/13 声明类)/`notChecked`,text 分组渲染,JSON 加 `buckets/metrics/headline`。
40
+
41
+ ### Verified
42
+
43
+ - `npm test` 193/193(含 root `test/`)。
44
+
1
45
  ## 1.2.2 — 2026-11-06
2
46
 
3
47
  - FLOW 沙箱:`flow collect --sandbox` 在 `unshare -Urn` + `prlimit` 下运行,记录 `sandbox{}`;env 白名单硬断言 token 不在子进程;进程组超时击杀;`producer.kind=sandbox` 仅当隔离项全部生效,否则 `sandbox-degraded` 且 audit 永不 runtime。测试 152 → 159。
package/README.en.md CHANGED
@@ -1,8 +1,8 @@
1
1
  <div align="center">
2
2
 
3
3
  <picture>
4
- <source media="(prefers-color-scheme: dark)" srcset="docs/assets/dsh-standards-flow-dark.svg">
5
- <img src="docs/assets/dsh-standards-flow-light.svg" width="100%" alt="dsh-standards pipeline: plugin dir → dsh-standards (DSC-01…13 static) → L1–L3 runtime → report (text/json/sarif, exit 0|1|2|3) → dsh-levels (ENTRY / FLOW / BOOST)">
4
+ <source media="(prefers-color-scheme: dark)" srcset="docs/assets/dsh-standards-hero-en-dark.webp">
5
+ <img src="docs/assets/dsh-standards-hero-en-light.webp" width="100%" alt="dsh-standards: DSC-01…13 static checks + L1–L3 runtime, reproducible reports">
6
6
  </picture>
7
7
 
8
8
  # dsh-standards
@@ -76,6 +76,11 @@ The full 51-document index lives in the Chinese [README.md](README.md).
76
76
 
77
77
  </details>
78
78
 
79
+ <picture>
80
+ <source media="(prefers-color-scheme: dark)" srcset="docs/assets/dsh-standards-flow-v4-dark.svg">
81
+ <img src="docs/assets/dsh-standards-flow-v4-light.svg" width="100%" alt="dsh-standards pipeline: plugin dir → DSC-01…13 → L1–L3 runtime → report → dsh-levels">
82
+ </picture>
83
+
79
84
  ## Three-step setup
80
85
 
81
86
  **1. Install** (either way; Node `>=24.9.0`):
package/README.es.md CHANGED
@@ -1,8 +1,8 @@
1
1
  <div align="center">
2
2
 
3
3
  <picture>
4
- <source media="(prefers-color-scheme: dark)" srcset="docs/assets/dsh-standards-flow-dark.svg">
5
- <img src="docs/assets/dsh-standards-flow-light.svg" width="100%" alt="Flujo de dsh-standards: directorio del plugin → dsh-standards (DSC-01…13 estáticas) → runtime L1–L3 → informe (text/json/sarif, exit 0|1|2|3) → dsh-levels (ENTRY / FLOW / BOOST)">
4
+ <source media="(prefers-color-scheme: dark)" srcset="docs/assets/dsh-standards-hero-en-dark.webp">
5
+ <img src="docs/assets/dsh-standards-hero-en-light.webp" width="100%" alt="dsh-standards: DSC-01…13 static checks + L1–L3 runtime, reproducible reports">
6
6
  </picture>
7
7
 
8
8
  # dsh-standards
@@ -76,6 +76,11 @@ El índice completo de 51 documentos está en el [README.md](README.md) en chino
76
76
 
77
77
  </details>
78
78
 
79
+ <picture>
80
+ <source media="(prefers-color-scheme: dark)" srcset="docs/assets/dsh-standards-flow-v4-dark.svg">
81
+ <img src="docs/assets/dsh-standards-flow-v4-light.svg" width="100%" alt="dsh-standards pipeline: plugin dir → DSC-01…13 → L1–L3 runtime → report → dsh-levels">
82
+ </picture>
83
+
79
84
  ## Instalación en tres pasos
80
85
 
81
86
  **1. Instalar** (dos opciones; Node `>=24.9.0`):
package/README.hi.md CHANGED
@@ -1,8 +1,8 @@
1
1
  <div align="center">
2
2
 
3
3
  <picture>
4
- <source media="(prefers-color-scheme: dark)" srcset="docs/assets/dsh-standards-flow-dark.svg">
5
- <img src="docs/assets/dsh-standards-flow-light.svg" width="100%" alt="dsh-standards प्रवाह: प्लगइन डायरेक्टरी → dsh-standards (DSC-01…13 स्थिर) → L1–L3 रनटाइम → रिपोर्ट (text/json/sarif, exit 0|1|2|3) → dsh-levels (ENTRY / FLOW / BOOST)">
4
+ <source media="(prefers-color-scheme: dark)" srcset="docs/assets/dsh-standards-hero-en-dark.webp">
5
+ <img src="docs/assets/dsh-standards-hero-en-light.webp" width="100%" alt="dsh-standards: DSC-01…13 static checks + L1–L3 runtime, reproducible reports">
6
6
  </picture>
7
7
 
8
8
  # dsh-standards
@@ -76,6 +76,11 @@ npm पर पुराना आरक्षण `dsh-standards@0.0.3` केव
76
76
 
77
77
  </details>
78
78
 
79
+ <picture>
80
+ <source media="(prefers-color-scheme: dark)" srcset="docs/assets/dsh-standards-flow-v4-dark.svg">
81
+ <img src="docs/assets/dsh-standards-flow-v4-light.svg" width="100%" alt="dsh-standards pipeline: plugin dir → DSC-01…13 → L1–L3 runtime → report → dsh-levels">
82
+ </picture>
83
+
79
84
  ## तीन चरणों में शुरुआत
80
85
 
81
86
  **1. इंस्टॉल** (दो में से कोई भी; Node `>=24.9.0`):
package/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  <div align="center">
2
2
 
3
3
  <picture>
4
- <source media="(prefers-color-scheme: dark)" srcset="docs/assets/dsh-standards-flow-dark.svg">
5
- <img src="docs/assets/dsh-standards-flow-light.svg" width="100%" alt="dsh-standards 流程:插件目录 → dsh-standards(DSC-01…13 静态)→ L1–L3 运行时 → 报告(text/json/sarif、exit 0|1|2|3)→ dsh-levels(ENTRY / FLOW / BOOST)">
4
+ <source media="(prefers-color-scheme: dark)" srcset="docs/assets/dsh-standards-hero-zh-dark.webp">
5
+ <img src="docs/assets/dsh-standards-hero-zh-light.webp" width="100%" alt="dsh-standards:DSC-01…13 静态检查 + L1–L3 运行时,报告可复现">
6
6
  </picture>
7
7
 
8
8
  # dsh-standards
@@ -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 个问题
@@ -94,6 +95,11 @@ npm 上的旧占位 `dsh-standards@0.0.3` 只是名字预留,这条 1.x 线取
94
95
 
95
96
  </details>
96
97
 
98
+ <picture>
99
+ <source media="(prefers-color-scheme: dark)" srcset="docs/assets/dsh-standards-flow-v4-dark.svg">
100
+ <img src="docs/assets/dsh-standards-flow-v4-light.svg" width="100%" alt="dsh-standards 流程:插件目录 → DSC-01…13 静态 → L1–L3 运行时 → 报告 → dsh-levels">
101
+ </picture>
102
+
97
103
  ## 三步上手
98
104
 
99
105
  **1. 安装**(二选一,Node 需 `>=24.9.0`):
@@ -111,6 +117,9 @@ npx -p dsh-standards dsh-levels --help
111
117
  **2. 跑一次检查**(目标是一个含 `package.json` 的包目录):
112
118
 
113
119
  ```sh
120
+ # 作者入口(推荐):默认 --entry-claim static --no-exec,不执行插件代码
121
+ dsh-standards check ./my-plugin
122
+
114
123
  # 入门兼容性:静态识别,不声明运行时证据
115
124
  dsh-standards ./my-plugin --scope entry --entry-claim static
116
125
 
@@ -122,6 +131,8 @@ dsh-standards ./my-plugin --format sarif --out report.sarif
122
131
  dsh-levels ./my-plugin --format json --out audit.json
123
132
  ```
124
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
+
125
136
  **3. 看结论**——先看 exit code,再看 `status`:
126
137
 
127
138
  ```text
@@ -186,19 +197,21 @@ coverage: mandatory=5 pass=5 warn=0 fail=0 na=0 notRun=0
186
197
 
187
198
  ## 配置
188
199
 
189
- 没有配置文件——全部可调项就是命令行参数。两个命令各一套:`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`。
190
201
 
191
- 最常用的 8 个:
202
+ 最常用的 10 个:
192
203
 
193
204
  | 参数 | 默认值 | 含义 |
194
205
  |---|---|---|
195
206
  | `<package-dir>` | `.` | 目标包目录,必须含 `package.json`;tarball 不在支持范围 |
196
207
  | `--scope` | `ds-c` | `mvp` = 8 个 MVP ID;`ds-c` = 全量声明;`entry` = 兼容性门 |
197
208
  | `--format` | `text` | `text` / `json` / `sarif`(SARIF 2.1.0) |
198
- | `--entry-claim` | `runtime` | `runtime` 需 DSC-14 运行时证据;`static` 只声明结构识别 |
199
- | `--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 |
200
211
  | `--profile` | `package` | `package` / `listing` / `both`;给了 `--market-index` 时默认 `listing` |
201
- | `--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` 探测 |
202
215
  | `--out` | — | 报告写入文件;不给就打 stdout |
203
216
 
204
217
  <details>
@@ -210,19 +223,21 @@ coverage: mandatory=5 pass=5 warn=0 fail=0 na=0 notRun=0
210
223
  | `--scope` | `ds-c` | 检查范围:`mvp` / `ds-c` / `entry` |
211
224
  | `--profile` | `package` | 评估 profile:`package` / `listing` / `both` |
212
225
  | `--runtime` | — | 指向构建好的 `@deepseek-ai/cordis` `lib/index.js`,开启 L1–L3 |
213
- | `--runtime-version` | `0.2.2-alpha.2` | 写进 anchor 的 dsh 运行时版本,供 C-2 判定 |
226
+ | `--runtime-version` | 宿主版本(`check`);否则内置常量 | 写进 anchor 的 dsh 运行时版本,供 C-2 判定;报告用 `anchor.runtimeSource` 说明来源 |
214
227
  | `--cordis-version` | `4.0.6-alpha.1` | 写进 anchor 的 cordis 版本 |
215
228
  | `--commit` | `ce7073d51254` | 写进 anchor 的 harness commit |
216
229
  | `--install-spec` | — | 给 C-3 用的推荐安装 spec |
217
230
  | `--market-index` | — | 格式 1 的市场索引 JSON,开启 listing 模式 |
218
231
  | `--plugin-name` | — | 市场索引里的插件名 |
219
232
  | `--timeout` | `10000` | L1–L3 超时毫秒(100–120000) |
220
- | `--no-exec` | `false` | 完全跳过 L1–L3,记 `not-run` / `NA` |
233
+ | `--no-exec` | `false`(裸检查器;`check` 默认 `true`) | 完全跳过 L1–L3,记 `not-run` / `NA` |
221
234
  | `--static-only` | `false` | M1 模式:只留 C-1/C-2/C-3/C-11/C-12/C-13,C-4–C-9 直接出界 |
222
235
  | `--treat-as-plugin` | `false` | 对 bundle 的 server 入口跑 L1–L3 |
223
236
  | `--entry-runtime` | `require` | entry 门要完整 G0–G3,还是接受 `server-entry` 代理 |
224
- | `--entry-claim` | `runtime` | entry 门要运行时证据(DSC-14),还是只做静态识别 |
225
- | `--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` 专用:显式允许探测宿主版本 |
226
241
  | `--base` | — | bundle 冒烟用的 base `cordis.patch.yml` |
227
242
  | `--web` | — | bundle 冒烟用的 web-app `cordis.patch.yml` |
228
243
  | `--bare` | — | host farm 的 bare-module 基线目录 |
@@ -292,7 +307,8 @@ coverage: mandatory=5 pass=5 warn=0 fail=0 na=0 notRun=0
292
307
  - **不声称官方推荐或采纳;不声称认证 / 认可 / 安全审计。**
293
308
  - **MVP 覆盖缺口**:`--scope mvp` 只落 8 个 ID,`DSC-03/10/11/12/13` 不在其中;它的结论是 `subset: "MVP-13"`、`level: null`,不得当 DS-C 徽章用。
294
309
  - **只吃包目录**:目标必须是含 `package.json` 的目录;tarball 支持不在 MVP 里。
295
- - **运行时结论有前置条件**:没有 `--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 版本未知」。
296
312
  - **结论带版本锚点**:`commit` / `dsh` / `cordis` 变了就是另一把尺子,旧报告不能直接当新版本的证据。
297
313
  - **BOOST 数据不是生产基线**:LEV 的 8/8 measured 来自 loader / context / provider-capture / synthetic timer 的合成环境,LEV-7 的 8.24× 同理。
298
314
  - **文本改动必须说明**:复制或修改本仓文本时,必须在显著位置给出 Change Notice(来源 commit、改了哪个章节、改了什么、为什么、日期),见 [`MODIFICATION-POLICY.md`](MODIFICATION-POLICY.md);标志与 badge 不随许可证授权,规则见 `governance/mark-policy.md`。
@@ -303,9 +319,10 @@ coverage: mandatory=5 pass=5 warn=0 fail=0 na=0 notRun=0
303
319
  git clone https://github.com/PerryLink/dsh-standards.git
304
320
  cd dsh-standards
305
321
 
306
- 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 # 安全默认:静态、不执行
307
324
  node bin/dsh-standards.mjs 40-checker/fixtures/good \
308
- --scope entry --entry-claim static --format text # 本仓 fixture 冒烟
325
+ --scope entry --entry-claim static --format text # 裸检查器 fixture 冒烟
309
326
  node bin/dsh-levels.mjs 40-checker/fixtures/good --format json
310
327
 
311
328
  # 从源码直连检查器:结构识别 / 声明运行(bundle 需要 host)