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 }))
|
package/40-checker/src/cli.mjs
CHANGED
|
@@ -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
|
|
301
|
+
runtimeChecks = await isolatedRuntimeChecks({
|
|
252
302
|
dir, manifest, kind: runtimeKind, entry: runtimeEntry,
|
|
253
|
-
runtimePath: options.runtime, timeoutMs: options.timeoutMs,
|
|
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
|
-
...
|
|
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', '
|
|
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
|
|
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.
|
|
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",
|