dsh-standards 1.0.1 → 1.0.2

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.
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env node
2
+ // Runs L1-L3 in a disposable child process so a plugin cannot block or take over
3
+ // the checker. Input: JSON on stdin. Output: JSON { checks } on stdout.
4
+ import { runRuntimeChecks } from '../src/runtime.mjs'
5
+ import { readSources } from '../src/util.mjs'
6
+
7
+ const chunks = []
8
+ for await (const chunk of process.stdin) chunks.push(chunk)
9
+ const input = JSON.parse(Buffer.concat(chunks).toString('utf8'))
10
+ let sources = []
11
+ try { sources = readSources(input.dir) } catch { sources = [] }
12
+ const checks = await runRuntimeChecks({ ...input, sources })
13
+ process.stdout.write(JSON.stringify({ checks }))
@@ -4,7 +4,7 @@ import { fileURLToPath } from 'node:url'
4
4
  import { runManifestChecks, DEFAULT_RUNTIME_VERSION } from './manifest.mjs'
5
5
  import { checkCapabilities, checkCredentials, checkImportTimeAdvisory, checkLabels } from './declarations.mjs'
6
6
  import { runRuntimeChecks } from './runtime.mjs'
7
- import { spawnSync } from 'node:child_process'
7
+ import { spawn, spawnSync } from 'node:child_process'
8
8
  import { readSources } from './util.mjs'
9
9
  import { buildReport, writeReport } from './report.mjs'
10
10
 
@@ -133,6 +133,51 @@ function parseArgs(argv) {
133
133
  return options
134
134
  }
135
135
 
136
+ function runtimeNotRun(reason) {
137
+ const meta = {
138
+ 'DSC-04': ['C-4', 'L1'], 'DSC-05': ['C-5', 'L1'], 'DSC-06': ['C-6', 'L1+L2'],
139
+ 'DSC-07': ['C-7', 'L2'], 'DSC-08': ['C-8', 'L3'], 'DSC-09': ['C-9', 'L1'],
140
+ }
141
+ return Object.entries(meta).map(([id, [clause, layer]]) => ({
142
+ id, clause, layer, result: 'ERROR', coverage: 'not-run', reason,
143
+ details: [], evidence: [], method: 'M', advisory: false,
144
+ }))
145
+ }
146
+
147
+ /**
148
+ * L1-L3 run in a child process so that a plugin cannot block the event loop,
149
+ * read the checker's environment, or kill the process. The child is hard-killed
150
+ * after timeoutMs + 3s; a kill is reported as not-run, never as a pass.
151
+ */
152
+ function isolatedRuntimeChecks(input) {
153
+ return new Promise((resolve) => {
154
+ const script = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', 'scripts', 'runtime-child.mjs')
155
+ const child = spawn(process.execPath, [script], { stdio: ['pipe', 'pipe', 'pipe'] })
156
+ let out = ''
157
+ let err = ''
158
+ const timer = setTimeout(() => { try { child.kill('SIGKILL') } catch {} }, input.timeoutMs + 3000)
159
+ child.stdout.on('data', (chunk) => { out += chunk })
160
+ child.stderr.on('data', (chunk) => { err += chunk })
161
+ child.on('error', (error) => {
162
+ clearTimeout(timer)
163
+ resolve(runtimeNotRun(`L1 child failed to start: ${error.message}`))
164
+ })
165
+ child.on('exit', (code, signal) => {
166
+ clearTimeout(timer)
167
+ if (signal === 'SIGKILL' || signal === 'SIGTERM') {
168
+ return resolve(runtimeNotRun(`L1 killed after ${input.timeoutMs}ms; plugin blocked the event loop`))
169
+ }
170
+ try {
171
+ const parsed = JSON.parse(out)
172
+ resolve(parsed.checks)
173
+ } catch {
174
+ resolve(runtimeNotRun(`L1 child produced no JSON (exit ${code}): ${String(err).slice(0, 200)}`))
175
+ }
176
+ })
177
+ child.stdin.end(JSON.stringify(input))
178
+ })
179
+ }
180
+
136
181
  async function entryRunCheck(kind, runtimeChecks, options, dir) {
137
182
  const byId = new Map(runtimeChecks.map(item => [item.id, item]))
138
183
  const c4 = byId.get('DSC-04')
@@ -204,6 +249,10 @@ export async function main(argv = process.argv.slice(2)) {
204
249
  process.stderr.write(`dsh-plugin-verify: target must be a package directory (tarball support is not in the MVP): ${dir}\n`)
205
250
  return 3
206
251
  }
252
+ if (!fs.existsSync(path.join(dir, 'package.json'))) {
253
+ process.stderr.write(`dsh-plugin-verify: not a package directory (no package.json): ${dir}\n`)
254
+ return 3
255
+ }
207
256
 
208
257
  const { manifest, kind, kindInferred, entry, patch, g4, checks: manifestChecks } = runManifestChecks(dir, {
209
258
  runtimeVersion: options.runtimeVersion,
@@ -223,6 +272,7 @@ export async function main(argv = process.argv.slice(2)) {
223
272
  { id: 'DSC-12', clause: 'C-12', layer: 'L0', result: 'ERROR', coverage: 'not-run', reason: 'manifest unavailable', details: [], evidence: [], method: 'M', advisory: false },
224
273
  { id: 'DSC-13', clause: 'C-13', layer: 'L0', result: 'ERROR', coverage: 'not-run', reason: 'manifest unavailable', details: [], evidence: [], method: 'M', advisory: false },
225
274
  ]
275
+ const declarationAdjusted = declarationChecks.map(item => (item.result === 'WARN' && ['DSC-11', 'DSC-13'].includes(item.id) ? { ...item, advisory: true } : item))
226
276
  const advisoryChecks = [checkImportTimeAdvisory(sources)]
227
277
 
228
278
  if (options.scope === 'entry' && options.entryRuntime === 'server-entry' && kind !== 'plugin') options.treatAsPlugin = true
@@ -248,9 +298,9 @@ export async function main(argv = process.argv.slice(2)) {
248
298
  result: 'ERROR', coverage: 'not-run', reason, details: [], evidence: [], method: 'M', advisory: false,
249
299
  }))
250
300
  } else {
251
- runtimeChecks = await runRuntimeChecks({
301
+ runtimeChecks = await isolatedRuntimeChecks({
252
302
  dir, manifest, kind: runtimeKind, entry: runtimeEntry,
253
- runtimePath: options.runtime, timeoutMs: options.timeoutMs, sources,
303
+ runtimePath: options.runtime, timeoutMs: options.timeoutMs,
254
304
  })
255
305
  }
256
306
 
@@ -260,9 +310,18 @@ export async function main(argv = process.argv.slice(2)) {
260
310
  ...runtimeChecks,
261
311
  runCheck,
262
312
  advisoryChecks[0],
263
- ...declarationChecks,
313
+ ...declarationAdjusted,
264
314
  ]
265
315
 
316
+ if (g4?.result === 'FAIL') {
317
+ const dsc01 = checks.find(item => item.id === 'DSC-01')
318
+ if (dsc01 && dsc01.result !== 'FAIL') {
319
+ dsc01.result = 'FAIL'
320
+ dsc01.reason = dsc01.reason ? `${dsc01.reason}; ${g4.reason ?? 'G4 failed'}` : (g4.reason ?? 'G4 failed')
321
+ dsc01.details = [...(dsc01.details ?? []), { item: 'G4', result: 'FAIL', reason: g4.reason ?? 'client shape failed' }]
322
+ }
323
+ }
324
+
266
325
  const subject = {
267
326
  mode: options.profile === 'listing' || options.profile === 'both' ? 'listing' : 'package',
268
327
  profile: options.profile,
@@ -206,6 +206,8 @@ export function runManifestChecks(dir, options = {}) {
206
206
  manifestDetails.push(detail('C-1.2', 'WARN', `entry not present in checkout; source/build hint exists (${entry.sourceEntry ?? 'build script'})`, ['package.json#scripts']))
207
207
  } else if (kind === 'bundle' || kind === 'client') {
208
208
  manifestDetails.push(detail('C-1.2', 'WARN', `no runtime entry; ${kind} packages may be patch/client only`, ['package.json#exports']))
209
+ } else if (Array.isArray(manifest.workspaces) || (manifest.workspaces && typeof manifest.workspaces === 'object')) {
210
+ manifestDetails.push(detail('C-1.2', 'NA', 'workspace root; run the checker on a package subdirectory', ['package.json#workspaces']))
209
211
  } else {
210
212
  manifestDetails.push(detail('C-1.2', 'FAIL', 'no resolvable runtime entry', ['package.json#exports', 'package.json#main']))
211
213
  }
@@ -224,7 +226,7 @@ export function runManifestChecks(dir, options = {}) {
224
226
  manifestDetails.push(detail('C-1.5', 'PASS', `license=${manifest.license}`))
225
227
  }
226
228
  if (manifest.private === true) {
227
- manifestDetails.push(detail('C-1.6', 'WARN', 'private:true; not publishable to a registry', ['package.json#private']))
229
+ manifestDetails.push(detail('C-1.6', 'NA', 'private:true; not publishable to a registry', ['package.json#private']))
228
230
  }
229
231
  const manifestResult = worst(manifestDetails.map(item => item.result))
230
232
  const manifestCheck = check({
@@ -45,7 +45,10 @@ export function buildReport({ subject, anchor, checks, g4 = null, scope, profile
45
45
  ? adjustedChecks.map(item => runtimeIds.has(item.id) ? { ...item, result: 'NA', coverage: 'not-run', reason: 'out of scope for M1 static-only' } : item)
46
46
  : adjustedChecks
47
47
  const byId = new Map(effectiveChecks.map(item => [item.id, item]))
48
- const selected = mandatory.map(id => byId.get(id)).filter(Boolean)
48
+ const selectedAll = mandatory.map(id => byId.get(id)).filter(Boolean)
49
+ // Advisory checks are reported but never move the verdict.
50
+ const selected = selectedAll.filter(item => item.advisory !== true)
51
+ const advisoryWarnings = selectedAll.filter(item => item.advisory === true && item.result === 'WARN')
49
52
  const missing = mandatory.filter(id => !byId.has(id))
50
53
  const failed = selected.filter(item => item.result === 'FAIL')
51
54
  const errored = selected.filter(item => item.result === 'ERROR')
@@ -80,6 +83,7 @@ export function buildReport({ subject, anchor, checks, g4 = null, scope, profile
80
83
  if (failed.length > 0) reasonParts.push(`FAIL: ${failed.map(item => item.id).join(', ')}`)
81
84
  if (notRunIds.length > 0) reasonParts.push(`not run: ${notRunIds.join(', ')}`)
82
85
  if (warnings.length > 0) reasonParts.push(`WARN: ${warnings.map(item => item.id).join(', ')}`)
86
+ if (advisoryWarnings.length > 0) reasonParts.push(`advisory: ${advisoryWarnings.map(item => item.id).join(', ')}`)
83
87
 
84
88
  return {
85
89
  schema: 'dsh-conformance/1',
@@ -92,6 +96,7 @@ export function buildReport({ subject, anchor, checks, g4 = null, scope, profile
92
96
  mandatory: mandatory.length,
93
97
  pass: pass.length,
94
98
  warn: warnings.length,
99
+ advisory: advisoryWarnings.length,
95
100
  fail: failed.length,
96
101
  na: selected.filter(item => item.result === 'NA').length,
97
102
  notRun: notRunIds.length,
package/CHANGELOG.md CHANGED
@@ -1,3 +1,11 @@
1
+ ## 1.0.2 — 2026-11-03
2
+
3
+ - L1–L3 run in a child process and are hard-killed on timeout; a blocked event loop is reported as `not-run`, never a pass.
4
+ - `dsh.capabilities` / `dsh.labels` warnings are advisory: still reported, no longer flip the verdict.
5
+ - A `g4` client-shape failure now fails DSC-01 instead of being dropped.
6
+ - `private: true` and workspace roots report NA instead of WARN/FAIL.
7
+ - A directory without `package.json` returns exit 3 (usage), matching `dsh-levels`.
8
+
1
9
  ## 1.0.1 — 2026-11-03
2
10
 
3
11
  - 打包:`files` 收窄,不再把 `40-checker/results/` 打进 npm 包(此前包含第三方插件结果、截图与本机路径)。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-standards",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
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",