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.
- package/40-checker/README.md +18 -1
- package/40-checker/scripts/build-offline-kit.mjs +284 -0
- package/40-checker/scripts/build-sidecar.mjs +714 -0
- package/40-checker/scripts/flow-collect.mjs +7 -1
- package/40-checker/scripts/level-audit.mjs +45 -7
- package/40-checker/scripts/run-tests.mjs +3 -7
- package/40-checker/src/author-cli.mjs +406 -0
- package/40-checker/src/author-report.mjs +271 -0
- package/40-checker/src/bundle-static.mjs +181 -0
- package/40-checker/src/cli.mjs +100 -11
- package/40-checker/src/client-shape.mjs +33 -3
- package/40-checker/src/declarations.mjs +53 -8
- package/40-checker/src/flow-evidence.mjs +106 -1
- package/40-checker/src/index.mjs +6 -4
- package/40-checker/src/manifest.mjs +87 -14
- package/40-checker/src/remediation.mjs +108 -7
- package/40-checker/src/report.mjs +203 -26
- package/40-checker/src/semver.mjs +112 -65
- package/40-checker/src/util.mjs +51 -8
- package/CHANGELOG.md +44 -0
- package/README.en.md +7 -2
- package/README.es.md +7 -2
- package/README.hi.md +7 -2
- package/README.md +35 -18
- package/README.pt.md +7 -2
- package/bin/dsh-standards.mjs +13 -2
- package/docs/assets/dsh-standards-flow-v3-dark.svg +87 -0
- package/docs/assets/dsh-standards-flow-v3-light.svg +87 -0
- package/docs/assets/dsh-standards-flow-v4-dark.svg +79 -0
- package/docs/assets/dsh-standards-flow-v4-light.svg +79 -0
- package/docs/assets/dsh-standards-hero-en-dark.webp +0 -0
- package/docs/assets/dsh-standards-hero-en-light.webp +0 -0
- package/docs/assets/dsh-standards-hero-zh-dark.webp +0 -0
- package/docs/assets/dsh-standards-hero-zh-light.webp +0 -0
- package/docs/coverage.md +82 -0
- package/package.json +1 -1
- package/spec/README.md +2 -0
- package/spec/coverage-contract.md +100 -0
- package/spec/dsc-clauses.json +766 -0
- package/spec/examples/boost-card.example.json +15 -0
- package/spec/examples/flow-evidence.example.json +113 -0
- package/spec/flow-evidence.schema.json +6 -2
- package/spec/remediation.json +54 -54
- package/spec/schemas/advice.schema.json +92 -0
- package/spec/schemas/boost-card.schema.json +107 -0
- package/spec/schemas/conformance.schema.json +848 -0
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Author-facing renderers for a `dsh-conformance/1` report.
|
|
3
|
+
*
|
|
4
|
+
* Markdown is the default `dsh-standards check` output; HTML is a single
|
|
5
|
+
* self-contained file with inline CSS, no scripts, no external assets, and
|
|
6
|
+
* every dynamic value escaped. Both read the same report object the checker
|
|
7
|
+
* already produced, so the verdict and exit code never change here.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const DISCLAIMER =
|
|
11
|
+
'Non-official report. This is not a certification, security audit, endorsement, or ranking. '
|
|
12
|
+
+ 'Hygiene warnings are not a clean pass; items under "Not checked" were not inspected.'
|
|
13
|
+
|
|
14
|
+
function text(value) {
|
|
15
|
+
return String(value ?? '').replace(/\s+/g, ' ').trim()
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function mdCell(value) {
|
|
19
|
+
return text(value).replace(/\|/g, '\\|')
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function escapeHtml(value) {
|
|
23
|
+
return String(value ?? '').replace(/[&<>"']/g, character => ({
|
|
24
|
+
'&': '&',
|
|
25
|
+
'<': '<',
|
|
26
|
+
'>': '>',
|
|
27
|
+
'"': '"',
|
|
28
|
+
"'": ''',
|
|
29
|
+
}[character]))
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function headlineOf(report) {
|
|
33
|
+
return text(report?.conclusion?.headline) || text(report?.conclusion?.reason) || 'no conclusion'
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function priorityFixes(report, limit = 3) {
|
|
37
|
+
const advice = Array.isArray(report?.advice) ? report.advice : []
|
|
38
|
+
const seen = new Set()
|
|
39
|
+
const fixes = []
|
|
40
|
+
for (const item of advice) {
|
|
41
|
+
const key = item?.key ?? item?.title
|
|
42
|
+
if (key === undefined || seen.has(key)) continue
|
|
43
|
+
seen.add(key)
|
|
44
|
+
fixes.push(item)
|
|
45
|
+
if (fixes.length >= limit) break
|
|
46
|
+
}
|
|
47
|
+
if (fixes.length > 0) return fixes
|
|
48
|
+
// Advice may be disabled; fall back to the FAIL/WARN rows themselves.
|
|
49
|
+
const rows = [...(report?.buckets?.blocking ?? []), ...(report?.buckets?.hygiene ?? [])]
|
|
50
|
+
for (const row of rows) {
|
|
51
|
+
if (fixes.length >= limit) break
|
|
52
|
+
fixes.push({ key: row.id, gate: row.result, severity: row.result, source: row.id, fix: row.reason, why: null, docsUrl: null })
|
|
53
|
+
}
|
|
54
|
+
return fixes
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function passedIds(report) {
|
|
58
|
+
return (report?.checks ?? []).filter(item => item.result === 'PASS').map(item => item.id)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function adviceRows(report) {
|
|
62
|
+
return Array.isArray(report?.advice) ? report.advice : []
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const BUCKETS = [
|
|
66
|
+
{ key: 'blocking', title: 'Blocking (FAIL)' },
|
|
67
|
+
{ key: 'hygiene', title: 'Hygiene (WARN)' },
|
|
68
|
+
{ key: 'partial', title: 'Partly checked (static subset)' },
|
|
69
|
+
{ key: 'notChecked', title: 'Not checked (NA / not-run / ERROR)' },
|
|
70
|
+
]
|
|
71
|
+
|
|
72
|
+
function bucketRows(report, key) {
|
|
73
|
+
return Array.isArray(report?.buckets?.[key]) ? report.buckets[key] : []
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function renderMarkdown(report) {
|
|
77
|
+
const subject = report?.subject ?? {}
|
|
78
|
+
const conclusion = report?.conclusion ?? {}
|
|
79
|
+
const coverage = report?.coverage ?? {}
|
|
80
|
+
const metrics = report?.metrics ?? {}
|
|
81
|
+
const anchor = report?.anchor ?? {}
|
|
82
|
+
const lines = []
|
|
83
|
+
lines.push(`# DSH standards check — ${text(subject.name) || '<unknown>'}@${text(subject.version) || '?'}`)
|
|
84
|
+
lines.push('')
|
|
85
|
+
lines.push(`**Conclusion:** ${headlineOf(report)}`)
|
|
86
|
+
lines.push('')
|
|
87
|
+
lines.push(`- Status: **${text(conclusion.status) || 'unknown'}** (exit ${conclusion.exitCode ?? '?'})`)
|
|
88
|
+
if (conclusion.level) lines.push(`- Level: ${text(conclusion.level)}`)
|
|
89
|
+
lines.push(`- Anchor: dsh=${text(anchor.dsh) || '?'}${anchor.runtimeSource ? ` (source=${anchor.runtimeSource}${anchor.suspect ? ', suspect=true' : ''})` : ''}`)
|
|
90
|
+
lines.push(`- Coverage: ${metrics.checked ?? coverage.checked ?? 0}/${metrics.total ?? coverage.total ?? (report?.checks ?? []).length} checks evaluated · declared fields ${metrics.declaredFields ?? 0}/5`)
|
|
91
|
+
lines.push('')
|
|
92
|
+
|
|
93
|
+
lines.push('## Priority fixes (up to 3)')
|
|
94
|
+
lines.push('')
|
|
95
|
+
const fixes = priorityFixes(report)
|
|
96
|
+
if (fixes.length === 0) {
|
|
97
|
+
lines.push('_None: no FAIL or WARN finding produced advice._')
|
|
98
|
+
} else {
|
|
99
|
+
fixes.forEach((item, index) => {
|
|
100
|
+
lines.push(`${index + 1}. **${text(item.severity ?? item.gate ?? 'WARN')} ${text(item.key) || text(item.title)}** (${text(item.source)}) — ${text(item.fix ?? item.title ?? item.reason)}`)
|
|
101
|
+
if (item.why) lines.push(` - Why: ${text(item.why)}`)
|
|
102
|
+
if (item.docsUrl) lines.push(` - Docs: ${text(item.docsUrl)}`)
|
|
103
|
+
})
|
|
104
|
+
}
|
|
105
|
+
lines.push('')
|
|
106
|
+
|
|
107
|
+
for (const bucket of BUCKETS) {
|
|
108
|
+
const rows = bucketRows(report, bucket.key)
|
|
109
|
+
lines.push(`## ${bucket.title} — ${rows.length}`)
|
|
110
|
+
lines.push('')
|
|
111
|
+
if (rows.length === 0) {
|
|
112
|
+
lines.push('_None._')
|
|
113
|
+
} else {
|
|
114
|
+
lines.push('| id | clause | result | coverage | reason |')
|
|
115
|
+
lines.push('|---|---|---|---|---|')
|
|
116
|
+
for (const row of rows) {
|
|
117
|
+
const tags = [row.declaration ? 'declaration' : null, row.advisory ? 'advisory' : null].filter(Boolean)
|
|
118
|
+
const reason = tags.length > 0 ? `${text(row.reason)} [${tags.join(',')}]` : text(row.reason)
|
|
119
|
+
lines.push(`| ${mdCell(row.id)} | ${mdCell(row.clause ?? '-')} | ${mdCell(row.result)} | ${mdCell(row.coverage ?? '-')} | ${mdCell(reason)} |`)
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
lines.push('')
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const passed = passedIds(report)
|
|
126
|
+
lines.push(`## Passed — ${passed.length}`)
|
|
127
|
+
lines.push('')
|
|
128
|
+
lines.push(passed.length > 0 ? passed.join(', ') : '_None._')
|
|
129
|
+
lines.push('')
|
|
130
|
+
|
|
131
|
+
const advice = adviceRows(report)
|
|
132
|
+
lines.push(`## Advice (non-blocking) — ${advice.length}`)
|
|
133
|
+
lines.push('')
|
|
134
|
+
if (advice.length === 0) {
|
|
135
|
+
lines.push('_None._')
|
|
136
|
+
} else {
|
|
137
|
+
for (const item of advice) {
|
|
138
|
+
const missing = item.ruleMissing ? ' [ruleMissing]' : ''
|
|
139
|
+
lines.push(`- ${text(item.severity)} ${text(item.key)} (${text(item.source)}${missing}): ${text(item.fix ?? item.title ?? '')}`)
|
|
140
|
+
if (item.docsUrl) lines.push(` ${text(item.docsUrl)}`)
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
lines.push('')
|
|
144
|
+
|
|
145
|
+
const notChecked = bucketRows(report, 'notChecked')
|
|
146
|
+
const partial = bucketRows(report, 'partial')
|
|
147
|
+
lines.push('## What this report does not check')
|
|
148
|
+
lines.push('')
|
|
149
|
+
if (notChecked.length === 0 && partial.length === 0) {
|
|
150
|
+
lines.push('- No check was skipped in this run.')
|
|
151
|
+
} else {
|
|
152
|
+
for (const row of notChecked) {
|
|
153
|
+
lines.push(`- ${text(row.id)}${row.clause ? ` (${text(row.clause)})` : ''}: ${text(row.reason) || 'not checked'}`)
|
|
154
|
+
if (row.howToEnable) lines.push(` - enable: ${text(row.howToEnable)}`)
|
|
155
|
+
}
|
|
156
|
+
for (const row of partial) {
|
|
157
|
+
lines.push(`- ${text(row.id)}${row.clause ? ` (${text(row.clause)})` : ''}: partial coverage${row.reason ? ` — ${text(row.reason)}` : ''}`)
|
|
158
|
+
if (row.howToEnable) lines.push(` - still needs: ${text(row.howToEnable)}`)
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
lines.push(`- Runtime execution beyond the checks listed above is not observed by this report${anchor.suspect ? '; the anchor differs from the host and is flagged suspect' : ''}.`)
|
|
162
|
+
lines.push('- It does not certify, rank, audit security, or endorse the package.')
|
|
163
|
+
lines.push('')
|
|
164
|
+
lines.push('---')
|
|
165
|
+
lines.push('')
|
|
166
|
+
lines.push(DISCLAIMER)
|
|
167
|
+
lines.push('')
|
|
168
|
+
return `${lines.join('\n')}\n`
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function renderHtml(report) {
|
|
172
|
+
const subject = report?.subject ?? {}
|
|
173
|
+
const conclusion = report?.conclusion ?? {}
|
|
174
|
+
const metrics = report?.metrics ?? {}
|
|
175
|
+
const anchor = report?.anchor ?? {}
|
|
176
|
+
const title = `DSH standards check — ${text(subject.name) || '<unknown>'}@${text(subject.version) || '?'}`
|
|
177
|
+
const fixes = priorityFixes(report)
|
|
178
|
+
const advice = adviceRows(report)
|
|
179
|
+
const passed = passedIds(report)
|
|
180
|
+
const notChecked = bucketRows(report, 'notChecked')
|
|
181
|
+
const partial = bucketRows(report, 'partial')
|
|
182
|
+
|
|
183
|
+
const table = (key) => {
|
|
184
|
+
const rows = bucketRows(report, key)
|
|
185
|
+
if (rows.length === 0) return '<p class="none">None.</p>'
|
|
186
|
+
const body = rows.map(row => {
|
|
187
|
+
const tags = [row.declaration ? 'declaration' : null, row.advisory ? 'advisory' : null].filter(Boolean)
|
|
188
|
+
const reason = tags.length > 0 ? `${text(row.reason)} [${tags.join(',')}]` : text(row.reason)
|
|
189
|
+
return `<tr><td>${escapeHtml(row.id)}</td><td>${escapeHtml(row.clause ?? '-')}</td><td class="result ${escapeHtml(String(row.result).toLowerCase())}">${escapeHtml(row.result)}</td><td>${escapeHtml(row.coverage ?? '-')}</td><td>${escapeHtml(reason)}</td></tr>`
|
|
190
|
+
}).join('\n')
|
|
191
|
+
return `<table><thead><tr><th>id</th><th>clause</th><th>result</th><th>coverage</th><th>reason</th></tr></thead><tbody>\n${body}\n</tbody></table>`
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const fixList = fixes.length === 0
|
|
195
|
+
? '<p class="none">None: no FAIL or WARN finding produced advice.</p>'
|
|
196
|
+
: `<ol>${fixes.map(item => {
|
|
197
|
+
const why = item.why ? `<br><span class="muted">Why: ${escapeHtml(text(item.why))}</span>` : ''
|
|
198
|
+
const docs = item.docsUrl ? `<br><span class="muted">Docs: ${link(item.docsUrl)}</span>` : ''
|
|
199
|
+
return `<li><strong>${escapeHtml(text(item.severity ?? item.gate ?? 'WARN'))} ${escapeHtml(text(item.key) || text(item.title))}</strong> (${escapeHtml(text(item.source))}) — ${escapeHtml(text(item.fix ?? item.title ?? item.reason))}${why}${docs}</li>`
|
|
200
|
+
}).join('\n')}</ol>`
|
|
201
|
+
|
|
202
|
+
const adviceList = advice.length === 0
|
|
203
|
+
? '<p class="none">None.</p>'
|
|
204
|
+
: `<ul>${advice.map(item => `<li><strong>${escapeHtml(text(item.severity))} ${escapeHtml(text(item.key))}</strong> (${escapeHtml(text(item.source))}${item.ruleMissing ? ' [ruleMissing]' : ''}): ${escapeHtml(text(item.fix ?? item.title ?? ''))}${item.docsUrl ? ` ${link(item.docsUrl)}` : ''}</li>`).join('\n')}</ul>`
|
|
205
|
+
|
|
206
|
+
const notCheckedList = notChecked.length === 0 && partial.length === 0
|
|
207
|
+
? '<li>No check was skipped in this run.</li>'
|
|
208
|
+
: [
|
|
209
|
+
...notChecked.map(row => `<li><strong>${escapeHtml(text(row.id))}</strong>${row.clause ? ` (${escapeHtml(text(row.clause))})` : ''}: ${escapeHtml(text(row.reason) || 'not checked')}${row.howToEnable ? ` <span class="muted">enable: ${escapeHtml(text(row.howToEnable))}</span>` : ''}</li>`),
|
|
210
|
+
...partial.map(row => `<li><strong>${escapeHtml(text(row.id))}</strong>${row.clause ? ` (${escapeHtml(text(row.clause))})` : ''}: partial coverage${row.reason ? ` — ${escapeHtml(text(row.reason))}` : ''}${row.howToEnable ? ` <span class="muted">still needs: ${escapeHtml(text(row.howToEnable))}</span>` : ''}</li>`),
|
|
211
|
+
].join('\n')
|
|
212
|
+
|
|
213
|
+
return `<!doctype html>
|
|
214
|
+
<html lang="en">
|
|
215
|
+
<head>
|
|
216
|
+
<meta charset="utf-8">
|
|
217
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
218
|
+
<title>${escapeHtml(title)}</title>
|
|
219
|
+
<style>
|
|
220
|
+
:root{color-scheme:light dark}
|
|
221
|
+
body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"PingFang SC","Microsoft YaHei",sans-serif;max-width:980px;margin:2rem auto;padding:0 1rem;line-height:1.55;color:#1f2328;background:#fff}
|
|
222
|
+
h1{font-size:1.45rem;margin:0 0 .35rem}
|
|
223
|
+
h2{font-size:1.1rem;margin:1.8rem 0 .5rem;border-bottom:1px solid #d0d7de;padding-bottom:.25rem}
|
|
224
|
+
.headline{font-size:1.05rem;font-weight:600;background:#f6f8fa;border:1px solid #d0d7de;border-radius:6px;padding:.65rem .8rem}
|
|
225
|
+
.meta{margin:.6rem 0 0;padding-left:1.1rem;color:#57606a}
|
|
226
|
+
.muted{color:#57606a;font-size:.9rem}
|
|
227
|
+
table{border-collapse:collapse;width:100%;margin:.4rem 0 1rem;font-size:.9rem}
|
|
228
|
+
th,td{border:1px solid #d0d7de;padding:.3rem .5rem;text-align:left;vertical-align:top}
|
|
229
|
+
th{background:#f6f8fa}
|
|
230
|
+
td.result{font-weight:600;white-space:nowrap}
|
|
231
|
+
td.result.fail{color:#b42318}
|
|
232
|
+
td.result.warn{color:#9a6700}
|
|
233
|
+
td.result.na,td.result.error,td.result.not-run{color:#57606a}
|
|
234
|
+
.none{color:#57606a}
|
|
235
|
+
footer{margin-top:2rem;padding-top:.8rem;border-top:1px solid #d0d7de;color:#57606a;font-size:.9rem}
|
|
236
|
+
code{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}
|
|
237
|
+
@media (prefers-color-scheme:dark){body{background:#0d1117;color:#e6edf3}h2,.headline,th,td,footer{border-color:#30363d}.headline,th{background:#161b22}.meta,.muted,.none,footer{color:#8b949e}td.result.fail{color:#ff7b72}td.result.warn{color:#d29922}}
|
|
238
|
+
</style>
|
|
239
|
+
</head>
|
|
240
|
+
<body>
|
|
241
|
+
<h1>${escapeHtml(title)}</h1>
|
|
242
|
+
<p class="headline">${escapeHtml(headlineOf(report))}</p>
|
|
243
|
+
<ul class="meta">
|
|
244
|
+
<li>Status: <strong>${escapeHtml(text(conclusion.status) || 'unknown')}</strong> (exit ${escapeHtml(conclusion.exitCode ?? '?')})${conclusion.level ? ` · Level: ${escapeHtml(text(conclusion.level))}` : ''}</li>
|
|
245
|
+
<li>Anchor: dsh=${escapeHtml(text(anchor.dsh) || '?')}${anchor.runtimeSource ? ` (source=${escapeHtml(anchor.runtimeSource)}${anchor.suspect ? ', suspect=true' : ''})` : ''}</li>
|
|
246
|
+
<li>Coverage: ${escapeHtml(metrics.checked ?? 0)}/${escapeHtml(metrics.total ?? (report?.checks ?? []).length)} checks evaluated · declared fields ${escapeHtml(metrics.declaredFields ?? 0)}/5</li>
|
|
247
|
+
</ul>
|
|
248
|
+
<h2>Priority fixes (up to 3)</h2>
|
|
249
|
+
${fixList}
|
|
250
|
+
${BUCKETS.map(bucket => `<h2>${escapeHtml(bucket.title)} — ${bucketRows(report, bucket.key).length}</h2>\n${table(bucket.key)}`).join('\n')}
|
|
251
|
+
<h2>Passed — ${passed.length}</h2>
|
|
252
|
+
<p>${passed.length > 0 ? passed.map(escapeHtml).join(', ') : '<span class="none">None.</span>'}</p>
|
|
253
|
+
<h2>Advice (non-blocking) — ${advice.length}</h2>
|
|
254
|
+
${adviceList}
|
|
255
|
+
<h2>What this report does not check</h2>
|
|
256
|
+
<ul>
|
|
257
|
+
${notCheckedList}
|
|
258
|
+
<li>Runtime execution beyond the checks listed above is not observed by this report${anchor.suspect ? '; the anchor differs from the host and is flagged suspect' : ''}.</li>
|
|
259
|
+
<li>It does not certify, rank, audit security, or endorse the package.</li>
|
|
260
|
+
</ul>
|
|
261
|
+
<footer>${escapeHtml(DISCLAIMER)}</footer>
|
|
262
|
+
</body>
|
|
263
|
+
</html>
|
|
264
|
+
`
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function link(url) {
|
|
268
|
+
const value = text(url)
|
|
269
|
+
if (!/^https:\/\//i.test(value)) return escapeHtml(value)
|
|
270
|
+
return `<a href="${escapeHtml(value)}" rel="noreferrer noopener">${escapeHtml(value)}</a>`
|
|
271
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Static partial coverage for the L1-L3 checks (DSC-04..DSC-09) when a bundle
|
|
3
|
+
* or client package cannot be executed.
|
|
4
|
+
*
|
|
5
|
+
* These checks are NOT a runtime verdict. They cover the statically decidable
|
|
6
|
+
* part of each clause (manifest/patch/entry/exports declarations, static
|
|
7
|
+
* provide/effect/teardown clues and declaration consistency) and always report
|
|
8
|
+
* `coverage: "partial"`. The report keeps them in the partial bucket and lists
|
|
9
|
+
* what still needs `--host` / `--runtime`.
|
|
10
|
+
*/
|
|
11
|
+
import path from 'node:path'
|
|
12
|
+
import { check, detail, worst } from './result.mjs'
|
|
13
|
+
import { isFile, regexHits } from './util.mjs'
|
|
14
|
+
|
|
15
|
+
const PRIVATE_SURFACE = /(?:ctx\.events\._hooks|ctx\.reflect\.store|ctx\.registry\._internal|fiber\._disposables)\s*(?:\.|\[)[^\n]{0,40}=/g
|
|
16
|
+
const EFFECT_REGISTRATION = /\bctx\.(?:effect|on)\s*\(|\.effect\s*\(/g
|
|
17
|
+
const TEARDOWN = /\b(?:onDispose|dispose|disposed)\b|\.dispose\s*\(/g
|
|
18
|
+
const RESTART = /\brestart\b|_revision|\.restart\s*\(/g
|
|
19
|
+
const PROVIDE_DECLARATION = /(?:^|\n)\s*(?:export\s+)?(?:const|let|var)\s+provide\s*=|export\s*\{[^}]*\bprovide\b[^}]*\}/g
|
|
20
|
+
const CTX_PROVIDE = /\bctx\.provide\s*\(/g
|
|
21
|
+
|
|
22
|
+
function packageNameOf(name) {
|
|
23
|
+
if (typeof name !== 'string') return ''
|
|
24
|
+
if (name.startsWith('@')) {
|
|
25
|
+
const parts = name.split('/')
|
|
26
|
+
return parts.length >= 2 ? `${parts[0]}/${parts[1]}` : name
|
|
27
|
+
}
|
|
28
|
+
return name.split('/')[0]
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function runtimeNeed(kind) {
|
|
32
|
+
if (kind === 'bundle') {
|
|
33
|
+
return 'bundle G0-G3 needs --host <dir>; L1-L3 need --treat-as-plugin --runtime <path>'
|
|
34
|
+
}
|
|
35
|
+
return 'L1-L3 need --treat-as-plugin --runtime <path>'
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function liveResult(details) {
|
|
39
|
+
const live = details.filter(item => item.result === 'FAIL' || item.result === 'WARN')
|
|
40
|
+
return live.length === 0 ? 'PASS' : worst(live.map(item => item.result))
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function staticCheck({ id, clause, layer, details, kind, scan, extra = '' }) {
|
|
44
|
+
const result = liveResult(details)
|
|
45
|
+
const failures = details
|
|
46
|
+
.filter(item => item.result === 'FAIL' || item.result === 'WARN')
|
|
47
|
+
.map(item => `${item.item}: ${item.reason}`)
|
|
48
|
+
const truncation = scan?.truncated
|
|
49
|
+
? `source scan truncated at ${scan.maxFiles} files / ${scan.maxBytes} bytes`
|
|
50
|
+
: ''
|
|
51
|
+
const reasonParts = []
|
|
52
|
+
if (failures.length > 0) reasonParts.push(failures.join(' | '))
|
|
53
|
+
else reasonParts.push(`static subset passed${extra ? `: ${extra}` : ''}`)
|
|
54
|
+
reasonParts.push(runtimeNeed(kind))
|
|
55
|
+
if (truncation) reasonParts.push(truncation)
|
|
56
|
+
return check({
|
|
57
|
+
id,
|
|
58
|
+
clause,
|
|
59
|
+
layer,
|
|
60
|
+
result,
|
|
61
|
+
coverage: 'partial',
|
|
62
|
+
reason: reasonParts.join('; '),
|
|
63
|
+
details,
|
|
64
|
+
evidence: details.flatMap(item => item.evidence ?? []).slice(0, 5),
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** @returns {object[]} the six DSC-04..DSC-09 checks with coverage "partial". */
|
|
69
|
+
export function runStaticBundleChecks({ manifest, kind, entry, patch, sources = [], g4 = null, scan = null, dir = null }) {
|
|
70
|
+
const name = manifest?.name ?? null
|
|
71
|
+
const deps = new Set([
|
|
72
|
+
...Object.keys(manifest?.dependencies ?? {}),
|
|
73
|
+
...Object.keys(manifest?.peerDependencies ?? {}),
|
|
74
|
+
...Object.keys(manifest?.optionalDependencies ?? {}),
|
|
75
|
+
])
|
|
76
|
+
const self = packageNameOf(name)
|
|
77
|
+
|
|
78
|
+
// C-4: entry / patch / client export shape, all statically resolvable.
|
|
79
|
+
const c4 = []
|
|
80
|
+
if (entry?.escape) c4.push(detail('C-4', 'FAIL', `entry escapes the package: ${entry.target}`, ['package.json#exports']))
|
|
81
|
+
else if (entry?.declaredMissing) c4.push(detail('C-4', 'FAIL', `declared entry is missing: ${entry.target}`, ['package.json#exports']))
|
|
82
|
+
else if (entry?.exists) c4.push(detail('C-4', 'PASS', `declared entry resolves: ${entry.relative}`, [entry.relative]))
|
|
83
|
+
else if (kind === 'bundle' && patch?.present) c4.push(detail('C-4', 'PASS', 'patch-only bundle; dsh.bundle.patch is the carrier'))
|
|
84
|
+
else c4.push(detail('C-4', 'NA', 'no runtime entry declared'))
|
|
85
|
+
if (patch?.present) {
|
|
86
|
+
if (patch.problems?.length > 0) {
|
|
87
|
+
c4.push(detail('C-4', 'FAIL', `bundle patch problems: ${patch.problems.join('; ')}`, patch.files?.map(file => file.relative) ?? []))
|
|
88
|
+
} else {
|
|
89
|
+
c4.push(detail('C-4', 'PASS', `bundle patch parsed (${patch.files?.length ?? 0} file(s), ${patch.rows?.length ?? 0} row(s))`))
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (g4?.hasClient) {
|
|
93
|
+
if (g4.result === 'FAIL') c4.push(detail('C-4', 'FAIL', g4.reason ?? 'dsh.client shape failed'))
|
|
94
|
+
else if (g4.result === 'PASS') c4.push(detail('C-4', 'PASS', 'dsh.client + exports["./client"] resolvable'))
|
|
95
|
+
}
|
|
96
|
+
const c4Check = staticCheck({ id: 'DSC-04', clause: 'C-4', layer: 'L1', details: c4, kind, scan, extra: 'entry/export/patch shape' })
|
|
97
|
+
|
|
98
|
+
// C-5: can a static provide declaration be enumerated?
|
|
99
|
+
const c5 = []
|
|
100
|
+
const provideDecl = regexHits(sources, PROVIDE_DECLARATION)
|
|
101
|
+
const ctxProvide = regexHits(sources, CTX_PROVIDE)
|
|
102
|
+
if (provideDecl.length > 0) {
|
|
103
|
+
c5.push(detail('C-5.1', 'PASS', `static provide declaration found (${provideDecl.length} site(s))`, provideDecl.slice(0, 3).map(hit => `${hit.file}:${hit.line}`)))
|
|
104
|
+
} else if (ctxProvide.length > 0) {
|
|
105
|
+
c5.push(detail('C-5.1', 'PASS', `ctx.provide registration found (${ctxProvide.length} site(s))`, ctxProvide.slice(0, 3).map(hit => `${hit.file}:${hit.line}`)))
|
|
106
|
+
} else {
|
|
107
|
+
c5.push(detail('C-5.1', 'NA', 'no static provide declaration or ctx.provide call found; service ownership needs runtime'))
|
|
108
|
+
}
|
|
109
|
+
const c5Check = staticCheck({ id: 'DSC-05', clause: 'C-5', layer: 'L1', details: c5, kind, scan, extra: 'provide declarations enumerated' })
|
|
110
|
+
|
|
111
|
+
// C-6: static private-surface writes; effect registration is runtime-only.
|
|
112
|
+
const c6 = []
|
|
113
|
+
const privateHits = regexHits(sources, PRIVATE_SURFACE)
|
|
114
|
+
if (privateHits.length > 0) {
|
|
115
|
+
c6.push(detail('C-6.1', 'FAIL', 'static write to a Cordis private registration surface', privateHits.slice(0, 3).map(hit => `${hit.file}:${hit.line}`)))
|
|
116
|
+
} else if (sources.length > 0) {
|
|
117
|
+
c6.push(detail('C-6.1', 'PASS', 'no static write to a private registration surface'))
|
|
118
|
+
}
|
|
119
|
+
const effectHits = regexHits(sources, EFFECT_REGISTRATION)
|
|
120
|
+
if (effectHits.length > 0) {
|
|
121
|
+
c6.push(detail('C-6.2', 'PASS', `${effectHits.length} static effect registration site(s); effect lifetime needs runtime`, effectHits.slice(0, 3).map(hit => `${hit.file}:${hit.line}`)))
|
|
122
|
+
} else {
|
|
123
|
+
c6.push(detail('C-6.2', 'NA', 'effect registration/lifetime needs runtime'))
|
|
124
|
+
}
|
|
125
|
+
const c6Check = staticCheck({ id: 'DSC-06', clause: 'C-6', layer: 'L1+L2', details: c6, kind, scan, extra: 'no private-surface writes found' })
|
|
126
|
+
|
|
127
|
+
// C-7: static teardown clue; effect/store/registry baselines need runtime.
|
|
128
|
+
const c7 = []
|
|
129
|
+
const teardownHits = regexHits(sources, TEARDOWN)
|
|
130
|
+
if (teardownHits.length > 0) {
|
|
131
|
+
c7.push(detail('C-7.1', 'PASS', `${teardownHits.length} static teardown site(s); dispose baseline needs runtime`, teardownHits.slice(0, 3).map(hit => `${hit.file}:${hit.line}`)))
|
|
132
|
+
} else {
|
|
133
|
+
c7.push(detail('C-7.1', 'NA', 'dispose/effect baseline needs runtime'))
|
|
134
|
+
}
|
|
135
|
+
const c7Check = staticCheck({ id: 'DSC-07', clause: 'C-7', layer: 'L2', details: c7, kind, scan, extra: 'teardown references enumerated' })
|
|
136
|
+
|
|
137
|
+
// C-8: static restart/remount clue; repeatability needs runtime.
|
|
138
|
+
const c8 = []
|
|
139
|
+
const restartHits = regexHits(sources, RESTART)
|
|
140
|
+
if (restartHits.length > 0) {
|
|
141
|
+
c8.push(detail('C-8.1', 'PASS', `${restartHits.length} static restart/revision site(s); repeatability needs runtime`, restartHits.slice(0, 3).map(hit => `${hit.file}:${hit.line}`)))
|
|
142
|
+
} else {
|
|
143
|
+
c8.push(detail('C-8.1', 'NA', 'restart/remount repeatability needs runtime'))
|
|
144
|
+
}
|
|
145
|
+
const c8Check = staticCheck({ id: 'DSC-08', clause: 'C-8', layer: 'L3', details: c8, kind, scan, extra: 'restart references enumerated' })
|
|
146
|
+
|
|
147
|
+
// C-9: declaration consistency that is decidable from the manifest + patch.
|
|
148
|
+
const c9 = []
|
|
149
|
+
const mismatched = []
|
|
150
|
+
for (const row of patch?.rows ?? []) {
|
|
151
|
+
if (!row.name || row.disabled) continue
|
|
152
|
+
// A patch row may mount a local module (a bundle's own server entry) rather
|
|
153
|
+
// than a registry package. Verify the file; do not compare it to deps.
|
|
154
|
+
if (row.name.startsWith('.') || path.isAbsolute(row.name)) {
|
|
155
|
+
const absolute = dir ? path.resolve(dir, row.name) : null
|
|
156
|
+
if (!absolute || !isFile(absolute)) mismatched.push(`${row.name} (missing local module)`)
|
|
157
|
+
continue
|
|
158
|
+
}
|
|
159
|
+
const target = packageNameOf(row.name)
|
|
160
|
+
if (!target || target === self || deps.has(target) || target.startsWith('@deepseek-ai/')) continue
|
|
161
|
+
mismatched.push(row.name)
|
|
162
|
+
}
|
|
163
|
+
if (mismatched.length > 0) {
|
|
164
|
+
c9.push(detail('C-9.2', 'WARN', `patch mounts package(s) that are not declared in dependencies/peerDependencies: ${[...new Set(mismatched)].join(', ')}`, patch.files?.map(file => file.relative) ?? []))
|
|
165
|
+
} else if (patch?.present) {
|
|
166
|
+
c9.push(detail('C-9.1', 'PASS', 'patch mount targets match package.json declarations or host-provided @deepseek-ai packages'))
|
|
167
|
+
}
|
|
168
|
+
const inject = Array.isArray(manifest?.dsh?.client?.inject) ? manifest.dsh.client.inject : []
|
|
169
|
+
const unread = []
|
|
170
|
+
for (const service of inject) {
|
|
171
|
+
if (typeof service !== 'string' || !/^[A-Za-z_$][\w$]*$/.test(service)) continue
|
|
172
|
+
if (regexHits(sources, new RegExp(`\\bctx\\.${service.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`, 'g')).length === 0) unread.push(service)
|
|
173
|
+
}
|
|
174
|
+
if (unread.length > 0) {
|
|
175
|
+
c9.push(detail('C-9.3', 'WARN', `dsh.client.inject declares service(s) with no static ctx.<service> read: ${unread.join(', ')}`))
|
|
176
|
+
}
|
|
177
|
+
if (c9.length === 0) c9.push(detail('C-9.1', 'NA', 'no statically decidable declaration mismatch found'))
|
|
178
|
+
const c9Check = staticCheck({ id: 'DSC-09', clause: 'C-9', layer: 'L1', details: c9, kind, scan, extra: 'declaration cross-check' })
|
|
179
|
+
|
|
180
|
+
return [c4Check, c5Check, c6Check, c7Check, c8Check, c9Check]
|
|
181
|
+
}
|