dsh-standards 0.0.3 → 1.0.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.
- package/40-checker/README.md +251 -0
- package/40-checker/bin/dsh-levels.mjs +15 -0
- package/40-checker/bin/dsh-plugin-verify.mjs +8 -0
- package/40-checker/fixtures/bad/index.mjs +12 -0
- package/40-checker/fixtures/bad/package.json +15 -0
- package/40-checker/fixtures/bad-manifest/index.js +1 -0
- package/40-checker/fixtures/bad-manifest/package.json +5 -0
- package/40-checker/fixtures/bundle/cordis.patch.yml +3 -0
- package/40-checker/fixtures/bundle/package.json +15 -0
- package/40-checker/fixtures/bundle/server.mjs +7 -0
- package/40-checker/fixtures/bundle-v0/cordis.patch.yml +3 -0
- package/40-checker/fixtures/bundle-v0/package.json +21 -0
- package/40-checker/fixtures/bundle-v0/server.mjs +7 -0
- package/40-checker/fixtures/client-missing/client.js +1 -0
- package/40-checker/fixtures/client-missing/index.js +1 -0
- package/40-checker/fixtures/client-missing/package.json +6 -0
- package/40-checker/fixtures/flow-config/index.js +9 -0
- package/40-checker/fixtures/flow-config/package.json +10 -0
- package/40-checker/fixtures/fs-write/index.mjs +7 -0
- package/40-checker/fixtures/fs-write/package.json +10 -0
- package/40-checker/fixtures/good/index.mjs +10 -0
- package/40-checker/fixtures/good/package.json +19 -0
- package/40-checker/fixtures/hang/index.mjs +5 -0
- package/40-checker/fixtures/hang/package.json +10 -0
- package/40-checker/fixtures/known-answer/expected.json +18 -0
- package/40-checker/fixtures/market-index-real.json +18 -0
- package/40-checker/fixtures/market-index.json +18 -0
- package/40-checker/fixtures/negative/bad-entry/package.json +1 -0
- package/40-checker/fixtures/negative/no-entry/package.json +1 -0
- package/40-checker/fixtures/negative/no-patch/package.json +1 -0
- package/40-checker/fixtures/negative/root-missing/cordis.patch.yml +2 -0
- package/40-checker/fixtures/negative/root-missing/package.json +1 -0
- package/40-checker/fixtures/negative/workspace/package.json +1 -0
- package/40-checker/package.json +22 -0
- package/40-checker/samples/README.md +8 -0
- package/40-checker/samples/entry18.json +139 -0
- package/40-checker/samples/frame.json +485 -0
- package/40-checker/scripts/apply-overlay.mjs +59 -0
- package/40-checker/scripts/attestation-check.mjs +151 -0
- package/40-checker/scripts/bundle-smoke.mjs +126 -0
- package/40-checker/scripts/claim-check.mjs +145 -0
- package/40-checker/scripts/clause-map.mjs +94 -0
- package/40-checker/scripts/collect-fixtures.mjs +44 -0
- package/40-checker/scripts/dashboard.mjs +85 -0
- package/40-checker/scripts/dco-check.mjs +147 -0
- package/40-checker/scripts/dependabot-check.mjs +40 -0
- package/40-checker/scripts/doc-commands-check.mjs +92 -0
- package/40-checker/scripts/entry-scan.mjs +190 -0
- package/40-checker/scripts/entry-validate.mjs +291 -0
- package/40-checker/scripts/entry18-attribution.mjs +66 -0
- package/40-checker/scripts/entry18-ci.mjs +685 -0
- package/40-checker/scripts/entry18-realweb-summary.mjs +71 -0
- package/40-checker/scripts/entry18-web.mjs +307 -0
- package/40-checker/scripts/evidence-chain.mjs +100 -0
- package/40-checker/scripts/flow-index.mjs +82 -0
- package/40-checker/scripts/flow-sample-ci.mjs +529 -0
- package/40-checker/scripts/governance-check.mjs +210 -0
- package/40-checker/scripts/host-farm.mjs +165 -0
- package/40-checker/scripts/journey-config-observe.mjs +109 -0
- package/40-checker/scripts/journey-disclosure-shape.mjs +83 -0
- package/40-checker/scripts/journey-runtime.mjs +178 -0
- package/40-checker/scripts/journey.mjs +76 -0
- package/40-checker/scripts/label-drift.mjs +74 -0
- package/40-checker/scripts/label-sync.mjs +87 -0
- package/40-checker/scripts/label-verify.mjs +90 -0
- package/40-checker/scripts/landlock-exec.c +101 -0
- package/40-checker/scripts/level-audit.mjs +336 -0
- package/40-checker/scripts/m1-sidecar.mjs +126 -0
- package/40-checker/scripts/m2-runner.mjs +213 -0
- package/40-checker/scripts/round-robin.mjs +88 -0
- package/40-checker/scripts/sample-frame.mjs +72 -0
- package/40-checker/scripts/sample20-ci.mjs +25 -0
- package/40-checker/scripts/sample60-ci.mjs +25 -0
- package/40-checker/scripts/stale-check.mjs +59 -0
- package/40-checker/scripts/surveillance.mjs +88 -0
- package/40-checker/scripts/web-probe.mjs +504 -0
- package/40-checker/src/cli.mjs +300 -0
- package/40-checker/src/client-shape.mjs +37 -0
- package/40-checker/src/declarations.mjs +242 -0
- package/40-checker/src/index.mjs +8 -0
- package/40-checker/src/install-spec.mjs +67 -0
- package/40-checker/src/label.mjs +130 -0
- package/40-checker/src/manifest.mjs +381 -0
- package/40-checker/src/permissions.mjs +60 -0
- package/40-checker/src/report.mjs +204 -0
- package/40-checker/src/result.mjs +60 -0
- package/40-checker/src/runtime.mjs +299 -0
- package/40-checker/src/semver.mjs +194 -0
- package/40-checker/src/util.mjs +148 -0
- package/40-checker/test/attestation.test.mjs +59 -0
- package/40-checker/test/checks.test.mjs +172 -0
- package/40-checker/test/claim.test.mjs +28 -0
- package/40-checker/test/client-shape.test.mjs +35 -0
- package/40-checker/test/dco.test.mjs +51 -0
- package/40-checker/test/entry-validate.test.mjs +172 -0
- package/40-checker/test/governance.test.mjs +76 -0
- package/40-checker/test/label.test.mjs +97 -0
- package/40-checker/test/level-audit.test.mjs +99 -0
- package/40-checker/test/levels.test.mjs +31 -0
- package/40-checker/test/ops.test.mjs +117 -0
- package/40-checker/test/permissions.test.mjs +40 -0
- package/40-checker/test/semver.test.mjs +32 -0
- package/40-checker/test/verify.test.mjs +57 -0
- package/CHANGELOG.md +24 -0
- package/LICENSE +28 -0
- package/LICENSE-APACHE +190 -0
- package/LICENSE-CC-BY +23 -0
- package/NOTICE +14 -0
- package/NOTICE.md +11 -0
- package/README.md +146 -3
- package/bin/dsh-levels.mjs +4 -0
- package/bin/dsh-standards.mjs +19 -0
- package/docs/BOOST-FIXTURES-2026-11-02.md +38 -0
- package/docs/DEVELOPER-JOURNEY.md +40 -0
- package/docs/HOST-J3-BUG-REPORT-DRAFT.md +27 -0
- package/docs/HOST-J3-CONFIG-REPRO-2026-11-02.md +36 -0
- package/docs/NAMING-FALLBACK.md +11 -0
- package/docs/fork-policy.md +12 -0
- package/docs/full-test-plan-2026-11-02.md +207 -0
- package/docs/incident.md +37 -0
- package/docs/onboarding.md +27 -0
- package/docs/privacy.md +12 -0
- package/docs/release.md +16 -0
- package/docs/scope-security.md +62 -0
- package/docs/status-taxonomy.md +54 -0
- package/docs/sunset.md +44 -0
- package/docs/translating.md +12 -0
- package/package.json +58 -12
- package/spec/README.md +27 -0
- package/spec/boost/README.md +8 -0
- package/spec/clauses.json +253 -0
- package/spec/entry/README.md +9 -0
- package/spec/flow/README.md +8 -0
- package/spec/levels.json +371 -0
- package/spec/levels.schema.json +45 -0
- package/spec/reference-suite.json +35 -0
- package/spec/schemas/attestation.schema.json +284 -0
- package/spec/schemas/evidence.schema.json +331 -0
- package/spec/schemas/report.schema.json +64 -0
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import path from 'node:path'
|
|
2
|
+
|
|
3
|
+
export const PACKAGE_MANDATORY = [
|
|
4
|
+
'DSC-01', 'DSC-02', 'DSC-04', 'DSC-05', 'DSC-06', 'DSC-07', 'DSC-08', 'DSC-09', 'DSC-11', 'DSC-12', 'DSC-13',
|
|
5
|
+
]
|
|
6
|
+
export const LISTING_MANDATORY = [...PACKAGE_MANDATORY, 'DSC-03']
|
|
7
|
+
export const MVP_CHECKS = ['DSC-01', 'DSC-02', 'DSC-04', 'DSC-05', 'DSC-06', 'DSC-07', 'DSC-08', 'DSC-09']
|
|
8
|
+
export const STATIC_MANDATORY = ['DSC-01', 'DSC-02', 'DSC-03', 'DSC-11', 'DSC-12', 'DSC-13']
|
|
9
|
+
export const ENTRY_RUN = ['DSC-01', 'DSC-02', 'DSC-03', 'DSC-11', 'DSC-12', 'DSC-13', 'DSC-14']
|
|
10
|
+
export const ENTRY_BLOCKING = ['C-1.1', 'C-1.2', 'C-1.4', 'C-3.1', 'C-3.2', 'C-3.5']
|
|
11
|
+
export const ENTRY_RUNTIME_BLOCKING = [...ENTRY_BLOCKING, 'C-2.2', 'C-2.3']
|
|
12
|
+
|
|
13
|
+
function mandatoryFor(scope, profile, staticOnly, entryClaim) {
|
|
14
|
+
if (scope === 'mvp') return MVP_CHECKS
|
|
15
|
+
if (scope === 'entry') return ENTRY_RUN.filter(id => (entryClaim !== 'static' || id !== 'DSC-14') && (id !== 'DSC-03' || profile === 'listing' || profile === 'both'))
|
|
16
|
+
if (staticOnly) return STATIC_MANDATORY
|
|
17
|
+
return profile === 'listing' || profile === 'both' ? LISTING_MANDATORY : PACKAGE_MANDATORY
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function adjustEntryCheck(item, entryClaim) {
|
|
21
|
+
if (['DSC-11', 'DSC-12', 'DSC-13'].includes(item.id) && item.result === 'FAIL') {
|
|
22
|
+
return { ...item, result: 'WARN', reason: 'entry treats safety/brand fields as WARN; they do not block loadability' }
|
|
23
|
+
}
|
|
24
|
+
if (item.id !== 'DSC-02' || item.result !== 'FAIL') return item
|
|
25
|
+
const details = item.details ?? []
|
|
26
|
+
if (entryClaim === 'static') return { ...item, result: 'WARN', reason: 'entry static claim: version declaration is WARN, not blocking' }
|
|
27
|
+
const hard = details.some(detail => detail.result === 'FAIL' && ['C-2.2', 'C-2.3'].includes(detail.item))
|
|
28
|
+
if (hard) return item
|
|
29
|
+
return { ...item, result: 'WARN', reason: 'C-2.1 version declaration missing/invalid; entry treats it as WARN (C-2.2/C-2.3 remain blocking)' }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function allowedNa(check, scope, profile, kind) {
|
|
33
|
+
if (check.result !== 'NA') return false
|
|
34
|
+
if (check.id === 'DSC-03' && scope !== 'mvp' && profile === 'package') return true
|
|
35
|
+
if (check.id === 'DSC-14' && ['client', 'preset', 'skill', 'theme', 'mcp-config', 'unknown'].includes(kind)) return true
|
|
36
|
+
if (['DSC-04', 'DSC-05', 'DSC-06', 'DSC-07', 'DSC-08', 'DSC-09'].includes(check.id) && ['bundle', 'client', 'preset', 'skill', 'theme', 'mcp-config', 'unknown'].includes(kind)) return true
|
|
37
|
+
return false
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function buildReport({ subject, anchor, checks, g4 = null, scope, profile, kind, staticOnly = false, entryRuntime = 'require', entryClaim = 'runtime' }) {
|
|
41
|
+
const mandatory = mandatoryFor(scope, profile, staticOnly, entryClaim)
|
|
42
|
+
const adjustedChecks = scope === 'entry' ? checks.map(item => adjustEntryCheck(item, entryClaim)) : checks
|
|
43
|
+
const runtimeIds = new Set(['DSC-04', 'DSC-05', 'DSC-06', 'DSC-07', 'DSC-08', 'DSC-09'])
|
|
44
|
+
const effectiveChecks = staticOnly
|
|
45
|
+
? adjustedChecks.map(item => runtimeIds.has(item.id) ? { ...item, result: 'NA', coverage: 'not-run', reason: 'out of scope for M1 static-only' } : item)
|
|
46
|
+
: adjustedChecks
|
|
47
|
+
const byId = new Map(effectiveChecks.map(item => [item.id, item]))
|
|
48
|
+
const selected = mandatory.map(id => byId.get(id)).filter(Boolean)
|
|
49
|
+
const missing = mandatory.filter(id => !byId.has(id))
|
|
50
|
+
const failed = selected.filter(item => item.result === 'FAIL')
|
|
51
|
+
const errored = selected.filter(item => item.result === 'ERROR')
|
|
52
|
+
const notRun = selected.filter(item => !allowedNa(item, scope, profile, kind) && (item.coverage === 'not-run' || item.coverage === 'partial'))
|
|
53
|
+
const notRunIds = [...new Set([...missing, ...errored.map(item => item.id), ...notRun.map(item => item.id)])]
|
|
54
|
+
const warnings = selected.filter(item => item.result === 'WARN')
|
|
55
|
+
const pass = selected.filter(item => item.result === 'PASS')
|
|
56
|
+
|
|
57
|
+
let status
|
|
58
|
+
let exitCode
|
|
59
|
+
if (failed.length > 0) { status = 'fail'; exitCode = 1 }
|
|
60
|
+
else if (notRunIds.length > 0) { status = 'incomplete'; exitCode = 2 }
|
|
61
|
+
else if (warnings.length > 0) { status = 'warn'; exitCode = 0 }
|
|
62
|
+
else { status = 'pass'; exitCode = 0 }
|
|
63
|
+
|
|
64
|
+
let level = null
|
|
65
|
+
if (scope === 'entry') {
|
|
66
|
+
const run = byId.get('DSC-14')
|
|
67
|
+
if (entryClaim === 'static') {
|
|
68
|
+
level = (status === 'pass' || status === 'warn') ? `DSH-ENTRY recognized${status === 'warn' ? ' (warnings)' : ''}` : null
|
|
69
|
+
} else if ((status === 'pass' || status === 'warn') && run?.result === 'PASS') {
|
|
70
|
+
level = `DSH-ENTRY runtime${entryRuntime === 'server-entry' ? ' (server-entry)' : ''}${status === 'warn' ? ' (warnings)' : ''}`
|
|
71
|
+
} else if (status === 'pass' || status === 'warn') {
|
|
72
|
+
level = `DSH-ENTRY static${status === 'warn' ? ' (warnings)' : ''}`
|
|
73
|
+
}
|
|
74
|
+
} else if (scope !== 'mvp' && (status === 'pass' || status === 'warn')) {
|
|
75
|
+
const suffix = status === 'warn' ? ', warnings' : ''
|
|
76
|
+
level = staticOnly ? `DS-C static${status === 'warn' ? ' (warnings)' : ''}` : profile === 'listing' || profile === 'both' ? `DS-C (listing${suffix})` : `DS-C (package${suffix})`
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const reasonParts = []
|
|
80
|
+
if (failed.length > 0) reasonParts.push(`FAIL: ${failed.map(item => item.id).join(', ')}`)
|
|
81
|
+
if (notRunIds.length > 0) reasonParts.push(`not run: ${notRunIds.join(', ')}`)
|
|
82
|
+
if (warnings.length > 0) reasonParts.push(`WARN: ${warnings.map(item => item.id).join(', ')}`)
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
schema: 'dsh-conformance/1',
|
|
86
|
+
standard: { id: 'DS-C', version: '1.0.0' },
|
|
87
|
+
subject,
|
|
88
|
+
anchor,
|
|
89
|
+
g4,
|
|
90
|
+
checks: effectiveChecks,
|
|
91
|
+
coverage: {
|
|
92
|
+
mandatory: mandatory.length,
|
|
93
|
+
pass: pass.length,
|
|
94
|
+
warn: warnings.length,
|
|
95
|
+
fail: failed.length,
|
|
96
|
+
na: selected.filter(item => item.result === 'NA').length,
|
|
97
|
+
notRun: notRunIds.length,
|
|
98
|
+
error: errored.length,
|
|
99
|
+
entryBlocking: scope === 'entry' ? (() => {
|
|
100
|
+
const blocking = entryClaim === 'static' ? ENTRY_BLOCKING : ENTRY_RUNTIME_BLOCKING
|
|
101
|
+
const failedItems = effectiveChecks.flatMap(item => (item.details ?? []).filter(detail => detail.result === 'FAIL' && blocking.includes(detail.item)).map(detail => detail.item))
|
|
102
|
+
return { required: blocking.length, failed: failedItems, pass: failedItems.length === 0 }
|
|
103
|
+
})() : null,
|
|
104
|
+
},
|
|
105
|
+
conclusion: {
|
|
106
|
+
level,
|
|
107
|
+
status,
|
|
108
|
+
exitCode,
|
|
109
|
+
profile,
|
|
110
|
+
scope,
|
|
111
|
+
subset: scope === 'mvp' ? 'MVP-13' : null,
|
|
112
|
+
staticOnly,
|
|
113
|
+
entryRuntime: scope === 'entry' ? entryRuntime : null,
|
|
114
|
+
entryClaim: scope === 'entry' ? entryClaim : null,
|
|
115
|
+
reason: reasonParts.join('; '),
|
|
116
|
+
},
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const ICON = { PASS: 'PASS', WARN: 'WARN', FAIL: 'FAIL', NA: 'NA ', ERROR: 'ERR ' }
|
|
121
|
+
|
|
122
|
+
export function formatText(report) {
|
|
123
|
+
const lines = []
|
|
124
|
+
const { subject, anchor, conclusion, coverage } = report
|
|
125
|
+
lines.push(`DS-C check — ${subject.name ?? '<unknown>'}@${subject.version ?? '?'} (${subject.kind}${subject.kindInferred ? ', inferred' : ''})`)
|
|
126
|
+
lines.push(`anchor: repo=${anchor.repo} commit=${anchor.commit.slice(0, 12)} dsh=${anchor.dsh} cordis=${anchor.cordis}`)
|
|
127
|
+
lines.push(`scope=${conclusion.scope} profile=${conclusion.profile} status=${conclusion.status} exit=${conclusion.exitCode}${conclusion.subset ? ` subset=${conclusion.subset}` : ''}${conclusion.staticOnly ? ' static-only' : ''}`)
|
|
128
|
+
if (conclusion.level) lines.push(`level: ${conclusion.level}`)
|
|
129
|
+
lines.push('')
|
|
130
|
+
lines.push('id result clause layer reason')
|
|
131
|
+
for (const item of report.checks) {
|
|
132
|
+
const reason = (item.reason || '').replace(/\s+/g, ' ').slice(0, 120)
|
|
133
|
+
lines.push(`${item.id.padEnd(7)} ${ICON[item.result] ?? item.result} ${item.clause.padEnd(6)} ${item.layer.padEnd(6)} ${reason}`)
|
|
134
|
+
for (const detail of item.details ?? []) {
|
|
135
|
+
if (detail.result === 'PASS') continue
|
|
136
|
+
lines.push(` - ${detail.item}: ${detail.result} ${String(detail.reason).slice(0, 140)}`)
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
lines.push('')
|
|
140
|
+
lines.push(`coverage: mandatory=${coverage.mandatory} pass=${coverage.pass} warn=${coverage.warn} fail=${coverage.fail} na=${coverage.na} notRun=${coverage.notRun}`)
|
|
141
|
+
if (conclusion.reason) lines.push(`reason: ${conclusion.reason}`)
|
|
142
|
+
if (conclusion.status === 'incomplete') lines.push('NOT A DS-C PASS: at least one mandatory check did not run (see notRun).')
|
|
143
|
+
lines.push('')
|
|
144
|
+
return `${lines.join('\n')}\n`
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function sarifLevel(item) {
|
|
148
|
+
if (item.result === 'FAIL' || item.result === 'ERROR') return 'error'
|
|
149
|
+
if (item.result === 'WARN') return 'warning'
|
|
150
|
+
return 'note'
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function locationOf(item) {
|
|
154
|
+
const evidence = item.evidence?.find(value => /^[^:]+:\d+$/.test(value))
|
|
155
|
+
if (!evidence) return undefined
|
|
156
|
+
const index = evidence.lastIndexOf(':')
|
|
157
|
+
const file = evidence.slice(0, index)
|
|
158
|
+
const line = Number(evidence.slice(index + 1))
|
|
159
|
+
return {
|
|
160
|
+
physicalLocation: {
|
|
161
|
+
artifactLocation: { uri: file.replace(/\\/g, '/') },
|
|
162
|
+
region: { startLine: Number.isFinite(line) && line > 0 ? line : 1 },
|
|
163
|
+
},
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function formatSarif(report) {
|
|
168
|
+
const rules = report.checks.map(item => ({
|
|
169
|
+
id: item.id,
|
|
170
|
+
name: item.id,
|
|
171
|
+
shortDescription: { text: `${item.clause} ${item.layer}` },
|
|
172
|
+
fullDescription: { text: `${item.clause} conformance check; coverage=${item.coverage}${item.advisory ? '; advisory' : ''}` },
|
|
173
|
+
properties: { clause: item.clause, layer: item.layer, coverage: item.coverage, advisory: item.advisory === true },
|
|
174
|
+
}))
|
|
175
|
+
const results = report.checks
|
|
176
|
+
.filter(item => item.result !== 'PASS' && item.result !== 'NA')
|
|
177
|
+
.map(item => ({
|
|
178
|
+
ruleId: item.id,
|
|
179
|
+
level: sarifLevel(item),
|
|
180
|
+
message: { text: item.reason || `${item.result} ${item.clause}` },
|
|
181
|
+
locations: locationOf(item) ? [locationOf(item)] : [],
|
|
182
|
+
properties: { clause: item.clause, result: item.result, coverage: item.coverage, advisory: item.advisory === true },
|
|
183
|
+
}))
|
|
184
|
+
return {
|
|
185
|
+
$schema: 'https://json.schemastore.org/sarif-2.1.0.json',
|
|
186
|
+
version: '2.1.0',
|
|
187
|
+
runs: [{
|
|
188
|
+
tool: { driver: { name: 'dsh-plugin-verify', version: '0.1.0', informationUri: 'https://github.com/dsh-community/dsh-plugin-verify', rules } },
|
|
189
|
+
properties: { standard: report.standard, anchor: report.anchor, conclusion: report.conclusion, subject: report.subject },
|
|
190
|
+
results,
|
|
191
|
+
}],
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function writeReport(report, format) {
|
|
196
|
+
if (format === 'json') return JSON.stringify(report, null, 2)
|
|
197
|
+
if (format === 'sarif') return JSON.stringify(formatSarif(report), null, 2)
|
|
198
|
+
return formatText(report)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function defaultOutPath(dir, format) {
|
|
202
|
+
const extension = format === 'json' ? 'json' : format === 'sarif' ? 'sarif' : 'txt'
|
|
203
|
+
return path.join(dir, `dsh-ds-c.${extension}`)
|
|
204
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/** Shared result shape for every check. */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @param {object} input
|
|
5
|
+
* @param {string} input.id
|
|
6
|
+
* @param {string} input.clause
|
|
7
|
+
* @param {string} input.layer
|
|
8
|
+
* @param {'PASS'|'WARN'|'FAIL'|'NA'|'ERROR'} input.result
|
|
9
|
+
* @param {string} [input.reason]
|
|
10
|
+
* @param {string[]} [input.evidence]
|
|
11
|
+
* @param {object[]} [input.details]
|
|
12
|
+
* @param {'full'|'partial'|'not-run'} [input.coverage]
|
|
13
|
+
* @param {'M'|'M+H'|'H'} [input.method]
|
|
14
|
+
* @param {boolean} [input.advisory]
|
|
15
|
+
*/
|
|
16
|
+
export function check(input) {
|
|
17
|
+
return {
|
|
18
|
+
id: input.id,
|
|
19
|
+
clause: input.clause,
|
|
20
|
+
layer: input.layer,
|
|
21
|
+
result: input.result,
|
|
22
|
+
coverage: input.coverage ?? (input.result === 'NA' ? 'not-run' : 'full'),
|
|
23
|
+
reason: input.reason ?? '',
|
|
24
|
+
evidence: input.evidence ?? [],
|
|
25
|
+
details: input.details ?? [],
|
|
26
|
+
method: input.method ?? 'M',
|
|
27
|
+
advisory: input.advisory === true,
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @param {string} item e.g. "C-1.1"
|
|
33
|
+
* @param {'PASS'|'WARN'|'FAIL'|'NA'|'ERROR'} result
|
|
34
|
+
* @param {string} reason
|
|
35
|
+
* @param {string[]} [evidence]
|
|
36
|
+
*/
|
|
37
|
+
export function detail(item, result, reason, evidence = []) {
|
|
38
|
+
return { item, result, reason, evidence }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const RANK = { ERROR: 5, FAIL: 4, WARN: 3, PASS: 2, NA: 1 }
|
|
42
|
+
|
|
43
|
+
export function worst(results) {
|
|
44
|
+
let current = 'NA'
|
|
45
|
+
for (const result of results) {
|
|
46
|
+
if (!result) continue
|
|
47
|
+
if (RANK[result] > RANK[current]) current = result
|
|
48
|
+
}
|
|
49
|
+
return current
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function countByResult(checks) {
|
|
53
|
+
const counts = { PASS: 0, WARN: 0, FAIL: 0, NA: 0, ERROR: 0 }
|
|
54
|
+
for (const item of checks) counts[item.result] = (counts[item.result] ?? 0) + 1
|
|
55
|
+
return counts
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function notRun(check, reason) {
|
|
59
|
+
return { ...check, result: 'ERROR', coverage: 'not-run', reason }
|
|
60
|
+
}
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import path from 'node:path'
|
|
2
|
+
import { pathToFileURL } from 'node:url'
|
|
3
|
+
import { check, detail, worst } from './result.mjs'
|
|
4
|
+
import { regexHits } from './util.mjs'
|
|
5
|
+
|
|
6
|
+
const ACTIVE = 2
|
|
7
|
+
|
|
8
|
+
function classifyImportError(message) {
|
|
9
|
+
const missing = /Cannot find (?:module|package) ['"]([^'"]+)['"]/.exec(message)
|
|
10
|
+
if (missing) {
|
|
11
|
+
const specifier = missing[1]
|
|
12
|
+
// A missing relative file is a packaging bug; a missing bare package is
|
|
13
|
+
// an environment/host-farm gap (the checker cannot tell whether the
|
|
14
|
+
// package manager would have installed it).
|
|
15
|
+
return specifier.startsWith('.') || specifier.startsWith('/') ? 'fail' : 'blocked'
|
|
16
|
+
}
|
|
17
|
+
if (/timed out/.test(message)) return 'blocked'
|
|
18
|
+
return 'fail'
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function makeStub() {
|
|
22
|
+
const fn = function stub() {}
|
|
23
|
+
return new Proxy(fn, {
|
|
24
|
+
get(_target, property) {
|
|
25
|
+
if (property === 'then') return undefined
|
|
26
|
+
if (property === 'toJSON') return () => ({})
|
|
27
|
+
if (property === Symbol.toPrimitive) return () => ''
|
|
28
|
+
if (property === Symbol.iterator) return function* empty() {}
|
|
29
|
+
return makeStub()
|
|
30
|
+
},
|
|
31
|
+
apply() { return makeStub() },
|
|
32
|
+
construct() { return makeStub() },
|
|
33
|
+
})
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function withTimeout(promise, timeoutMs, label) {
|
|
37
|
+
let timer
|
|
38
|
+
const timeout = new Promise((_, reject) => {
|
|
39
|
+
timer = setTimeout(() => reject(new Error(`${label} timed out after ${timeoutMs}ms`)), timeoutMs)
|
|
40
|
+
if (typeof timer.unref === 'function') timer.unref()
|
|
41
|
+
})
|
|
42
|
+
return Promise.race([promise, timeout]).finally(() => clearTimeout(timer))
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function declaredInject(module) {
|
|
46
|
+
const value = module?.inject ?? module?.default?.inject
|
|
47
|
+
if (Array.isArray(value)) return value.filter(item => typeof item === 'string')
|
|
48
|
+
if (value && typeof value === 'object') return Object.keys(value)
|
|
49
|
+
return []
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function declaredProvide(module) {
|
|
53
|
+
const value = module?.provide ?? module?.default?.provide
|
|
54
|
+
if (typeof value === 'string') return [value]
|
|
55
|
+
if (Array.isArray(value)) return value.filter(item => typeof item === 'string')
|
|
56
|
+
return []
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function pluginOf(module) {
|
|
60
|
+
if (typeof module?.default === 'function') return module.default
|
|
61
|
+
if (typeof module?.apply === 'function') return module
|
|
62
|
+
if (typeof module === 'function') return module
|
|
63
|
+
return null
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function implFiber(impl) {
|
|
67
|
+
return impl?.ctx?.fiber ?? impl?.fiber ?? null
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function storeEntries(ctx) {
|
|
71
|
+
const store = ctx.reflect.store ?? {}
|
|
72
|
+
return Reflect.ownKeys(store).map(key => store[key]).filter(Boolean)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function ownedService(ctx, fiber, name) {
|
|
76
|
+
return storeEntries(ctx).find(impl => impl?.name === name && implFiber(impl) === fiber) ?? null
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function serviceNames(ctx, fiber) {
|
|
80
|
+
return new Set(storeEntries(ctx).filter(impl => implFiber(impl) === fiber).map(impl => impl.name))
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function makeNa(reason) {
|
|
84
|
+
return [
|
|
85
|
+
check({ id: 'DSC-04', clause: 'C-4', layer: 'L1', result: 'NA', reason }),
|
|
86
|
+
check({ id: 'DSC-05', clause: 'C-5', layer: 'L1', result: 'NA', reason }),
|
|
87
|
+
check({ id: 'DSC-06', clause: 'C-6', layer: 'L1+L2', result: 'NA', reason }),
|
|
88
|
+
check({ id: 'DSC-07', clause: 'C-7', layer: 'L2', result: 'NA', reason }),
|
|
89
|
+
check({ id: 'DSC-08', clause: 'C-8', layer: 'L3', result: 'NA', reason }),
|
|
90
|
+
check({ id: 'DSC-09', clause: 'C-9', layer: 'L1', result: 'NA', reason }),
|
|
91
|
+
]
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function makeNotRun(reason, skip = []) {
|
|
95
|
+
return ['DSC-04', 'DSC-05', 'DSC-06', 'DSC-07', 'DSC-08', 'DSC-09'].filter(id => !skip.includes(id)).map(id => {
|
|
96
|
+
const clause = { 'DSC-04': 'C-4', 'DSC-05': 'C-5', 'DSC-06': 'C-6', 'DSC-07': 'C-7', 'DSC-08': 'C-8', 'DSC-09': 'C-9' }[id]
|
|
97
|
+
const layer = { 'DSC-04': 'L1', 'DSC-05': 'L1', 'DSC-06': 'L1+L2', 'DSC-07': 'L2', 'DSC-08': 'L3', 'DSC-09': 'L1' }[id]
|
|
98
|
+
return check({ id, clause, layer, result: 'ERROR', coverage: 'not-run', reason })
|
|
99
|
+
})
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Run L1-L3 in a child-free context. This is a best-effort harness: any
|
|
104
|
+
* failure that cannot be attributed to the plugin (missing host packages,
|
|
105
|
+
* incomplete stubs) is reported as ERROR/blocked, never FAIL.
|
|
106
|
+
*
|
|
107
|
+
* @param {object} input
|
|
108
|
+
* @param {string} input.dir
|
|
109
|
+
* @param {object} input.manifest
|
|
110
|
+
* @param {string} input.kind
|
|
111
|
+
* @param {object|null} input.entry
|
|
112
|
+
* @param {string} input.runtimePath
|
|
113
|
+
* @param {number} [input.timeoutMs]
|
|
114
|
+
* @param {object[]} [input.sources]
|
|
115
|
+
*/
|
|
116
|
+
export async function runRuntimeChecks(input) {
|
|
117
|
+
const { dir, manifest, kind, entry, runtimePath, timeoutMs = 10_000, sources = [] } = input
|
|
118
|
+
if (kind !== 'plugin') return makeNa(`kind=${kind}; L1-L3 apply to plugin entries only`)
|
|
119
|
+
if (!runtimePath) return makeNotRun('no --runtime supplied; L1-L3 not run')
|
|
120
|
+
if (!entry?.exists) return makeNotRun('no resolvable runtime entry; L1-L3 not run')
|
|
121
|
+
|
|
122
|
+
let Context
|
|
123
|
+
try {
|
|
124
|
+
const runtime = await import(pathToFileURL(path.resolve(runtimePath)).href)
|
|
125
|
+
Context = runtime.Context ?? runtime.default?.Context
|
|
126
|
+
if (typeof Context !== 'function') throw new Error('runtime module does not export Context')
|
|
127
|
+
} catch (error) {
|
|
128
|
+
return makeNotRun(`runtime import failed: ${error.message}`)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
let module
|
|
132
|
+
try {
|
|
133
|
+
module = await import(pathToFileURL(entry.absolute).href)
|
|
134
|
+
} catch (error) {
|
|
135
|
+
const message = error?.message ?? String(error)
|
|
136
|
+
if (classifyImportError(message) === 'blocked') {
|
|
137
|
+
return makeNotRun(`plugin import blocked by missing host dependency: ${message.split('\n')[0]}`)
|
|
138
|
+
}
|
|
139
|
+
return [
|
|
140
|
+
check({ id: 'DSC-04', clause: 'C-4', layer: 'L1', result: 'FAIL', reason: `entry import failed: ${message}`, evidence: [entry.relative] }),
|
|
141
|
+
...makeNotRun('C-4 failed; L1-L3 short-circuited', ['DSC-04']),
|
|
142
|
+
]
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const plugin = pluginOf(module)
|
|
146
|
+
if (!plugin) return makeNa('entry does not export a function, { apply }, or default class')
|
|
147
|
+
|
|
148
|
+
let ctx
|
|
149
|
+
let fiber
|
|
150
|
+
let beforeStore = null
|
|
151
|
+
let beforeRegistry = 0
|
|
152
|
+
try {
|
|
153
|
+
ctx = new Context()
|
|
154
|
+
for (const name of declaredInject(module)) {
|
|
155
|
+
if (ctx.get(name, false) === undefined) {
|
|
156
|
+
try { ctx.provide(name, makeStub()) } catch { /* already provided by the root */ }
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
beforeStore = new Set(Reflect.ownKeys(ctx.reflect.store ?? {}))
|
|
160
|
+
beforeRegistry = ctx.registry.size
|
|
161
|
+
fiber = ctx.plugin(plugin)
|
|
162
|
+
await withTimeout(Promise.resolve(fiber), timeoutMs, 'plugin load')
|
|
163
|
+
} catch (error) {
|
|
164
|
+
const message = error?.message ?? String(error)
|
|
165
|
+
const stubGap = /is not a function|Cannot read propert(?:y|ies) of (?:undefined|null)/.test(message)
|
|
166
|
+
const blocked = classifyImportError(message) === 'blocked' || stubGap
|
|
167
|
+
return [
|
|
168
|
+
check({ id: 'DSC-04', clause: 'C-4', layer: 'L1', result: blocked ? 'ERROR' : 'FAIL', coverage: blocked ? 'not-run' : 'full', reason: `load failed: ${message}`, evidence: [entry.relative] }),
|
|
169
|
+
...makeNotRun('C-4 did not reach ACTIVE', ['DSC-04']),
|
|
170
|
+
]
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const active = fiber.state === ACTIVE
|
|
174
|
+
const loadCheck = check({
|
|
175
|
+
id: 'DSC-04', clause: 'C-4', layer: 'L1',
|
|
176
|
+
result: active ? 'PASS' : 'FAIL',
|
|
177
|
+
reason: active ? '' : `fiber state ${fiber.state} is not ACTIVE (2)`,
|
|
178
|
+
evidence: [entry.relative],
|
|
179
|
+
})
|
|
180
|
+
if (!active) return [loadCheck, ...makeNotRun('C-4 did not reach ACTIVE', ['DSC-04'])]
|
|
181
|
+
|
|
182
|
+
const realFiber = fiber.ctx?.fiber ?? Object.getPrototypeOf(fiber) ?? fiber
|
|
183
|
+
const declared = declaredProvide(module)
|
|
184
|
+
const measured = serviceNames(ctx, realFiber)
|
|
185
|
+
const provideDetails = []
|
|
186
|
+
for (const name of declared) {
|
|
187
|
+
const impl = ownedService(ctx, realFiber, name)
|
|
188
|
+
if (impl) provideDetails.push(detail('C-5.2', 'PASS', `service ${name} is owned by the plugin fiber`))
|
|
189
|
+
else if (storeEntries(ctx).some(entry => entry?.name === name)) provideDetails.push(detail('C-5.2', 'FAIL', `service ${name} is owned by another fiber`))
|
|
190
|
+
else provideDetails.push(detail('C-5.2', 'FAIL', `declared provide ${name} was not registered`))
|
|
191
|
+
}
|
|
192
|
+
if (declared.length === 0) provideDetails.push(detail('C-5.1', 'NA', 'no provide declaration'))
|
|
193
|
+
const provideResult = declared.length === 0 ? 'NA' : worst(provideDetails.map(item => item.result))
|
|
194
|
+
const provideCheck = check({
|
|
195
|
+
id: 'DSC-05', clause: 'C-5', layer: 'L1', result: provideResult,
|
|
196
|
+
reason: provideDetails.filter(item => item.result !== 'PASS' && item.result !== 'NA').map(item => `${item.item}: ${item.reason}`).join(' | '),
|
|
197
|
+
details: provideDetails,
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
const effects = fiber.getEffects()
|
|
201
|
+
const labels = effects.map(effect => effect?.label ?? 'anonymous')
|
|
202
|
+
const privateHits = regexHits(sources, /(?:ctx\.events\._hooks|ctx\.reflect\.store|ctx\.registry\._internal|fiber\._disposables)\s*(?:\.|\[)[^\n]{0,40}=/g)
|
|
203
|
+
const effectDetails = []
|
|
204
|
+
if (effects.length === 0 && (declared.length > 0 || labels.length === 0)) effectDetails.push(detail('C-6.2', 'WARN', 'no registered effects observed'))
|
|
205
|
+
else effectDetails.push(detail('C-6.2', 'PASS', `${effects.length} registered effect(s)`))
|
|
206
|
+
if (labels.length > 0 && labels.every(label => label === 'anonymous')) effectDetails.push(detail('C-6.2', 'WARN', 'all effects are anonymous'))
|
|
207
|
+
if (privateHits.length > 0) effectDetails.push(detail('C-6.1', 'FAIL', 'direct write to a Cordis private registration surface', privateHits.slice(0, 3).map(hit => `${hit.file}:${hit.line}`)))
|
|
208
|
+
const effectResult = worst(effectDetails.map(item => item.result))
|
|
209
|
+
const effectCheck = check({
|
|
210
|
+
id: 'DSC-06', clause: 'C-6', layer: 'L1+L2', result: effectResult,
|
|
211
|
+
reason: effectDetails.filter(item => item.result !== 'PASS').map(item => `${item.item}: ${item.reason}`).join(' | '),
|
|
212
|
+
details: effectDetails,
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
let unloadDetails = []
|
|
216
|
+
let unloadResult = 'PASS'
|
|
217
|
+
try {
|
|
218
|
+
await withTimeout(Promise.resolve(fiber.dispose()), timeoutMs, 'fiber dispose')
|
|
219
|
+
const afterEffects = fiber.getEffects().length
|
|
220
|
+
const afterStore = new Set(Reflect.ownKeys(ctx.reflect.store ?? {}))
|
|
221
|
+
const afterRegistry = ctx.registry.size
|
|
222
|
+
unloadDetails = [
|
|
223
|
+
detail('C-7.1', afterEffects === 0 ? 'PASS' : 'FAIL', `getEffects().length=${afterEffects}`),
|
|
224
|
+
detail('C-7.2', [...beforeStore].every(key => afterStore.has(key)) ? 'PASS' : 'FAIL', 'service store returned to baseline'),
|
|
225
|
+
detail('C-7.3', beforeRegistry === afterRegistry ? 'PASS' : 'FAIL', `registry.size ${beforeRegistry} -> ${afterRegistry}`),
|
|
226
|
+
]
|
|
227
|
+
unloadResult = worst(unloadDetails.map(item => item.result))
|
|
228
|
+
} catch (error) {
|
|
229
|
+
unloadDetails = [detail('C-7.5', 'FAIL', `dispose failed: ${error.message}`)]
|
|
230
|
+
unloadResult = 'FAIL'
|
|
231
|
+
}
|
|
232
|
+
const unloadCheck = check({
|
|
233
|
+
id: 'DSC-07', clause: 'C-7', layer: 'L2', result: unloadResult,
|
|
234
|
+
reason: unloadDetails.filter(item => item.result !== 'PASS').map(item => `${item.item}: ${item.reason}`).join(' | '),
|
|
235
|
+
details: unloadDetails,
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
let restartResult = 'PASS'
|
|
239
|
+
let restartDetails = []
|
|
240
|
+
try {
|
|
241
|
+
const ctxR = new Context()
|
|
242
|
+
for (const name of declaredInject(module)) {
|
|
243
|
+
if (ctxR.get(name, false) === undefined) {
|
|
244
|
+
try { ctxR.provide(name, makeStub()) } catch { /* ignore */ }
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
const fiberR = ctxR.plugin(plugin)
|
|
248
|
+
await withTimeout(Promise.resolve(fiberR), timeoutMs, 'restart fixture load')
|
|
249
|
+
const beforeRestart = fiberR.getEffects().length
|
|
250
|
+
const revisionBefore = typeof fiberR._revision === 'number' ? fiberR._revision : null
|
|
251
|
+
const epochBefore = fiberR._runner?.epoch
|
|
252
|
+
await withTimeout(Promise.resolve(fiberR.restart()), timeoutMs, 'fiber restart')
|
|
253
|
+
const revisionAfter = typeof fiberR._revision === 'number' ? fiberR._revision : null
|
|
254
|
+
const epochAfter = fiberR._runner?.epoch
|
|
255
|
+
const afterRestart = fiberR.getEffects()
|
|
256
|
+
restartDetails.push(detail('C-8.1', fiberR.state === ACTIVE ? 'PASS' : 'FAIL', `state after restart=${fiberR.state}`))
|
|
257
|
+
restartDetails.push(detail('C-8.1', afterRestart.length === beforeRestart ? 'PASS' : 'WARN', `effect count ${beforeRestart} -> ${afterRestart.length}`))
|
|
258
|
+
const advanced = (revisionBefore !== null && revisionAfter !== null && revisionAfter > revisionBefore) || (epochBefore !== undefined && epochAfter !== undefined && epochAfter !== epochBefore)
|
|
259
|
+
restartDetails.push(detail('C-8.2', advanced ? 'PASS' : 'WARN', `revision ${revisionBefore} -> ${revisionAfter}; epoch ${String(epochBefore)} -> ${String(epochAfter)}`))
|
|
260
|
+
const ctx2 = new Context()
|
|
261
|
+
for (const name of declaredInject(module)) {
|
|
262
|
+
if (ctx2.get(name, false) === undefined) {
|
|
263
|
+
try { ctx2.provide(name, makeStub()) } catch { /* ignore */ }
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
const fiber2 = ctx2.plugin(plugin)
|
|
267
|
+
await withTimeout(Promise.resolve(fiber2), timeoutMs, 'cold remount')
|
|
268
|
+
restartDetails.push(detail('C-8.3', fiber2.state === ACTIVE ? 'PASS' : 'FAIL', `cold remount state=${fiber2.state}`))
|
|
269
|
+
} catch (error) {
|
|
270
|
+
restartDetails.push(detail('C-8.1', 'FAIL', `restart/remount failed: ${error.message}`))
|
|
271
|
+
}
|
|
272
|
+
restartResult = worst(restartDetails.map(item => item.result))
|
|
273
|
+
const restartCheck = check({
|
|
274
|
+
id: 'DSC-08', clause: 'C-8', layer: 'L3', result: restartResult,
|
|
275
|
+
reason: restartDetails.filter(item => item.result !== 'PASS').map(item => `${item.item}: ${item.reason}`).join(' | '),
|
|
276
|
+
details: restartDetails,
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
const crossDetails = []
|
|
280
|
+
for (const name of declared) {
|
|
281
|
+
if (!measured.has(name)) crossDetails.push(detail('C-9.1', 'FAIL', `declared provide ${name} is missing from measured services`))
|
|
282
|
+
}
|
|
283
|
+
for (const name of measured) {
|
|
284
|
+
if (!declared.includes(name)) crossDetails.push(detail('C-9.2', 'WARN', `measured service ${name} is not declared`))
|
|
285
|
+
}
|
|
286
|
+
const inject = declaredInject(module)
|
|
287
|
+
for (const name of inject) {
|
|
288
|
+
const hits = regexHits(sources, new RegExp(`ctx\\.${name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`, 'g'))
|
|
289
|
+
if (hits.length === 0) crossDetails.push(detail('C-9.3', 'WARN', `inject declares ${name} but no static ctx.${name} read was found`))
|
|
290
|
+
}
|
|
291
|
+
const crossResult = crossDetails.length === 0 ? 'PASS' : worst(crossDetails.map(item => item.result))
|
|
292
|
+
const crossCheck = check({
|
|
293
|
+
id: 'DSC-09', clause: 'C-9', layer: 'L1', result: crossResult,
|
|
294
|
+
reason: crossDetails.filter(item => item.result !== 'PASS').map(item => `${item.item}: ${item.reason}`).join(' | '),
|
|
295
|
+
details: crossDetails,
|
|
296
|
+
})
|
|
297
|
+
|
|
298
|
+
return [loadCheck, provideCheck, effectCheck, unloadCheck, restartCheck, crossCheck]
|
|
299
|
+
}
|