dsh-plugin-compat-check 0.1.0 → 0.1.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.
- package/LICENSE +21 -0
- package/README.md +21 -57
- package/index.js +93 -155
- package/kernel/lib/baseline.mjs +110 -34
- package/kernel/lib/contracts.mjs +493 -0
- package/kernel/lib/declarations.mjs +50 -0
- package/kernel/lib/extract-worker.mjs +10 -0
- package/kernel/lib/extract.mjs +235 -1515
- package/kernel/lib/flow-values.mjs +53 -0
- package/kernel/lib/observation.mjs +182 -0
- package/kernel/lib/report-service.mjs +55 -0
- package/kernel/lib/report.mjs +237 -0
- package/kernel/lib/source-analysis.mjs +1913 -0
- package/kernel/lib/source-graph.mjs +297 -0
- package/kernel/lib/state-store.mjs +126 -0
- package/kernel/lib/syntax.mjs +73 -0
- package/kernel/lib/verdict.mjs +492 -587
- package/kernel/lib/version.mjs +2 -2
- package/kernel/lib/worker-analysis.mjs +82 -0
- package/kernel/rules/README.md +46 -58
- package/kernel/rules/severity.json +161 -35
- package/kernel/rules/usage-inventory.json +369 -61
- package/lib/client.js +523 -10
- package/lib/host-resources.js +92 -0
- package/lib/report-contract.js +494 -0
- package/package.json +14 -5
- package/scripts/prepublish-check.mjs +14 -9
- package/scripts/stage-contract.mjs +43 -0
- package/scripts/stage-kernel.mjs +9 -21
- package/skill/SKILL.md +40 -39
|
@@ -0,0 +1,493 @@
|
|
|
1
|
+
/** Browser-safe runtime type contracts. No imports: staged into the prebuilt
|
|
2
|
+
* client and plugin host from this single source. Unknown additive fields are
|
|
3
|
+
* preserved. Version changes or missing evidence fail closed as tool errors.
|
|
4
|
+
* @typedef {{status:'complete'|'limited',verifiedCalls:number,limitations:string[]}} Coverage
|
|
5
|
+
* @typedef {{category:string,severity:'fatal'|'feature-dead'|'degraded',certainty:'certain'|'suspected',impactBasis:'measured'|'inferred',location:object|null}} Finding
|
|
6
|
+
* @typedef {{mode:'catalogue'|'runtime',bound:boolean,usable:boolean,notes:string[],profileId:string|null,dshVersion:string|null,probedAt:string|null}} Observation
|
|
7
|
+
*/
|
|
8
|
+
export const REPORT_SCHEMA = 1;
|
|
9
|
+
export const REPORT_KIND = "plugin-compat-check/result";
|
|
10
|
+
export const ENVELOPE_KIND = "plugin-compat-check/report";
|
|
11
|
+
const record = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
|
|
12
|
+
const text = (v) => typeof v === "string" && v.length > 0;
|
|
13
|
+
const nullableText = (v) => v === null || typeof v === "string";
|
|
14
|
+
const integer = (v) => Number.isSafeInteger(v) && v >= 0;
|
|
15
|
+
const strings = (v) =>
|
|
16
|
+
Array.isArray(v) && v.every((x) => typeof x === "string");
|
|
17
|
+
const verdicts = ["ok", "partial", "broken", "unknown"];
|
|
18
|
+
const severities = ["fatal", "feature-dead", "degraded"];
|
|
19
|
+
const certainties = ["certain", "suspected"];
|
|
20
|
+
const dispositions = [
|
|
21
|
+
"finding",
|
|
22
|
+
"client-side",
|
|
23
|
+
"self-provided",
|
|
24
|
+
"runtime-facility",
|
|
25
|
+
"filtered",
|
|
26
|
+
"abstained",
|
|
27
|
+
"guarded",
|
|
28
|
+
"no-calls",
|
|
29
|
+
"unknown",
|
|
30
|
+
];
|
|
31
|
+
function coherentVerdict(p) {
|
|
32
|
+
if (!Array.isArray(p.findings)) return false;
|
|
33
|
+
return (
|
|
34
|
+
p.verdict === "unknown" ||
|
|
35
|
+
p.verdict ===
|
|
36
|
+
(p.findings.some((f) => f?.severity === "fatal")
|
|
37
|
+
? "broken"
|
|
38
|
+
: p.findings.length
|
|
39
|
+
? "partial"
|
|
40
|
+
: "ok")
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
function snapshotFinding(f) {
|
|
44
|
+
return (
|
|
45
|
+
record(f) &&
|
|
46
|
+
text(f.key) &&
|
|
47
|
+
text(f.category) &&
|
|
48
|
+
severities.includes(f.severity) &&
|
|
49
|
+
certainties.includes(f.certainty) &&
|
|
50
|
+
nullableText(f.location)
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
function checkedEntry(v) {
|
|
54
|
+
return record(v) && text(v.service) && dispositions.includes(v.disposition);
|
|
55
|
+
}
|
|
56
|
+
// Producers resolve relative observation paths before serialization. Lexical
|
|
57
|
+
// normalization here is platform independent and needs no browser Node shim.
|
|
58
|
+
function pathIdentity(v) {
|
|
59
|
+
const normalized = v.replace(/\\/g, "/");
|
|
60
|
+
const parts = [];
|
|
61
|
+
for (const part of normalized.split("/")) {
|
|
62
|
+
if (part === "..") parts.pop();
|
|
63
|
+
else if (part && part !== ".") parts.push(part);
|
|
64
|
+
}
|
|
65
|
+
return (normalized.startsWith("/") ? "/" : "") + parts.join("/");
|
|
66
|
+
}
|
|
67
|
+
function need(errors, condition, path) {
|
|
68
|
+
if (!condition) errors.push(path);
|
|
69
|
+
}
|
|
70
|
+
export function exitCodeForSummary(summary) {
|
|
71
|
+
return summary.broken > 0
|
|
72
|
+
? 2
|
|
73
|
+
: summary.partial > 0 || summary.unknown > 0
|
|
74
|
+
? 1
|
|
75
|
+
: 0;
|
|
76
|
+
}
|
|
77
|
+
export function createFailure(error, options = {}) {
|
|
78
|
+
return {
|
|
79
|
+
...options,
|
|
80
|
+
schemaVersion: REPORT_SCHEMA,
|
|
81
|
+
kind: REPORT_KIND,
|
|
82
|
+
ok: false,
|
|
83
|
+
exitCode: 3,
|
|
84
|
+
toolVersion: options.toolVersion ?? null,
|
|
85
|
+
error: String(error || "Unknown tool error"),
|
|
86
|
+
inputs: options.inputs || {},
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
export function validateObservation(value) {
|
|
90
|
+
const e = [];
|
|
91
|
+
if (!record(value)) return ["observation"];
|
|
92
|
+
need(e, ["runtime", "catalogue"].includes(value.mode), "observation.mode");
|
|
93
|
+
need(e, typeof value.bound === "boolean", "observation.bound");
|
|
94
|
+
need(e, typeof value.usable === "boolean", "observation.usable");
|
|
95
|
+
need(e, strings(value.notes), "observation.notes");
|
|
96
|
+
for (const key of ["profileId", "dshVersion", "probedAt"])
|
|
97
|
+
need(e, nullableText(value[key]), "observation." + key);
|
|
98
|
+
if (value.probedAt !== null)
|
|
99
|
+
need(
|
|
100
|
+
e,
|
|
101
|
+
Number.isFinite(Date.parse(value.probedAt)),
|
|
102
|
+
"observation.probedAt",
|
|
103
|
+
);
|
|
104
|
+
need(
|
|
105
|
+
e,
|
|
106
|
+
(value.mode === "runtime") === value.usable,
|
|
107
|
+
"observation.mode/usable",
|
|
108
|
+
);
|
|
109
|
+
return e;
|
|
110
|
+
}
|
|
111
|
+
export function validateFinding(f) {
|
|
112
|
+
const e = [];
|
|
113
|
+
if (!record(f)) return ["finding"];
|
|
114
|
+
need(e, text(f.category), "finding.category");
|
|
115
|
+
need(e, severities.includes(f.severity), "finding.severity");
|
|
116
|
+
need(e, certainties.includes(f.certainty), "finding.certainty");
|
|
117
|
+
need(
|
|
118
|
+
e,
|
|
119
|
+
["inferred", "measured"].includes(f.impactBasis),
|
|
120
|
+
"finding.impactBasis",
|
|
121
|
+
);
|
|
122
|
+
need(e, text(f.impact), "finding.impact");
|
|
123
|
+
need(e, text(f.basis), "finding.basis");
|
|
124
|
+
for (const key of ["service", "method"])
|
|
125
|
+
if (f[key] !== undefined) need(e, nullableText(f[key]), "finding." + key);
|
|
126
|
+
need(
|
|
127
|
+
e,
|
|
128
|
+
f.location === null ||
|
|
129
|
+
(record(f.location) &&
|
|
130
|
+
text(f.location.file) &&
|
|
131
|
+
Number.isSafeInteger(f.location.line) &&
|
|
132
|
+
f.location.line > 0),
|
|
133
|
+
"finding.location",
|
|
134
|
+
);
|
|
135
|
+
if (f.evidence !== undefined) need(e, record(f.evidence), "finding.evidence");
|
|
136
|
+
if (record(f.location)) {
|
|
137
|
+
for (const key of ["column", "start", "end"])
|
|
138
|
+
if (f.location[key] !== undefined && f.location[key] !== null)
|
|
139
|
+
need(e, integer(f.location[key]), "finding.location." + key);
|
|
140
|
+
if (integer(f.location.start) && integer(f.location.end))
|
|
141
|
+
need(e, f.location.end >= f.location.start, "finding.location.span");
|
|
142
|
+
}
|
|
143
|
+
return e;
|
|
144
|
+
}
|
|
145
|
+
export function validateBaseline(value) {
|
|
146
|
+
const e = [];
|
|
147
|
+
if (!record(value) || value.schemaVersion !== 2)
|
|
148
|
+
return ["baseline.schemaVersion"];
|
|
149
|
+
need(e, nullableText(value.dshVersion), "baseline.dshVersion");
|
|
150
|
+
need(
|
|
151
|
+
e,
|
|
152
|
+
record(value.identity) && text(value.identity.mode),
|
|
153
|
+
"baseline.identity",
|
|
154
|
+
);
|
|
155
|
+
if (!record(value.plugins)) return [...e, "baseline.plugins"];
|
|
156
|
+
for (const [name, p] of Object.entries(value.plugins)) {
|
|
157
|
+
if (!record(p)) {
|
|
158
|
+
e.push("baseline.plugin:" + name);
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
need(
|
|
162
|
+
e,
|
|
163
|
+
nullableText(p.version) && verdicts.includes(p.verdict),
|
|
164
|
+
"baseline.plugin:" + name,
|
|
165
|
+
);
|
|
166
|
+
need(e, Array.isArray(p.findings), "baseline.findings:" + name);
|
|
167
|
+
need(e, coherentVerdict(p), "baseline.verdict/findings:" + name);
|
|
168
|
+
if (p.vacuous !== undefined)
|
|
169
|
+
need(e, typeof p.vacuous === "boolean", "baseline.vacuous:" + name);
|
|
170
|
+
for (const f of Array.isArray(p.findings) ? p.findings : []) {
|
|
171
|
+
if (!record(f)) {
|
|
172
|
+
e.push("baseline.finding:" + name);
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
need(e, snapshotFinding(f), "baseline.finding:" + name);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return e;
|
|
179
|
+
}
|
|
180
|
+
function validateBaselineResult(b) {
|
|
181
|
+
if (b === null) return [];
|
|
182
|
+
const e = [];
|
|
183
|
+
if (!record(b)) return ["baseline result"];
|
|
184
|
+
if (b.error !== undefined) {
|
|
185
|
+
need(e, text(b.error) && text(b.note), "baseline.error");
|
|
186
|
+
return e;
|
|
187
|
+
}
|
|
188
|
+
need(e, typeof b.wrote === "boolean", "baseline.wrote");
|
|
189
|
+
need(e, record(b.badge), "baseline.badge");
|
|
190
|
+
if (record(b.badge)) {
|
|
191
|
+
need(
|
|
192
|
+
e,
|
|
193
|
+
typeof b.badge.show === "boolean" &&
|
|
194
|
+
nullableText(b.badge.text) &&
|
|
195
|
+
nullableText(b.badge.detail) &&
|
|
196
|
+
[null, "worse", "better", "mixed", "neutral"].includes(
|
|
197
|
+
b.badge.direction,
|
|
198
|
+
),
|
|
199
|
+
"baseline.badge.fields",
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
if (b.diff !== null) {
|
|
203
|
+
need(
|
|
204
|
+
e,
|
|
205
|
+
record(b.diff) && typeof b.diff.comparable === "boolean",
|
|
206
|
+
"baseline.diff",
|
|
207
|
+
);
|
|
208
|
+
if (record(b.diff)) {
|
|
209
|
+
for (const key of [
|
|
210
|
+
"added",
|
|
211
|
+
"removed",
|
|
212
|
+
"worsened",
|
|
213
|
+
"improved",
|
|
214
|
+
"updated",
|
|
215
|
+
]) {
|
|
216
|
+
need(e, Array.isArray(b.diff[key]), "baseline.diff." + key);
|
|
217
|
+
for (const entry of Array.isArray(b.diff[key]) ? b.diff[key] : []) {
|
|
218
|
+
const at = "baseline.diff." + key;
|
|
219
|
+
if (!record(entry)) {
|
|
220
|
+
e.push(at + ".entry");
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
need(e, text(entry.name), at + ".name");
|
|
224
|
+
if (key === "added" || key === "removed") {
|
|
225
|
+
need(e, nullableText(entry.version), at + ".version");
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
need(
|
|
229
|
+
e,
|
|
230
|
+
nullableText(entry.versionFrom) &&
|
|
231
|
+
nullableText(entry.versionTo) &&
|
|
232
|
+
typeof entry.versionChanged === "boolean" &&
|
|
233
|
+
verdicts.includes(entry.verdictFrom) &&
|
|
234
|
+
verdicts.includes(entry.verdictTo) &&
|
|
235
|
+
text(entry.reason),
|
|
236
|
+
at + ".fields",
|
|
237
|
+
);
|
|
238
|
+
for (const field of ["newFindings", "goneFindings"]) {
|
|
239
|
+
need(
|
|
240
|
+
e,
|
|
241
|
+
Array.isArray(entry[field]) &&
|
|
242
|
+
entry[field].every(snapshotFinding),
|
|
243
|
+
at + "." + field,
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
need(
|
|
247
|
+
e,
|
|
248
|
+
Array.isArray(entry.changedFindings) &&
|
|
249
|
+
entry.changedFindings.every(
|
|
250
|
+
(c) =>
|
|
251
|
+
record(c) && snapshotFinding(c.from) && snapshotFinding(c.to),
|
|
252
|
+
),
|
|
253
|
+
at + ".changedFindings",
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
for (const key of ["dshVersionFrom", "dshVersionTo", "reason"])
|
|
258
|
+
need(e, nullableText(b.diff[key]), "baseline.diff." + key);
|
|
259
|
+
need(
|
|
260
|
+
e,
|
|
261
|
+
typeof b.diff.dshVersionChanged === "boolean",
|
|
262
|
+
"baseline.diff.dshVersionChanged",
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return e;
|
|
267
|
+
}
|
|
268
|
+
export function validateReportBody(body) {
|
|
269
|
+
const e = [];
|
|
270
|
+
if (!record(body)) return ["report"];
|
|
271
|
+
need(
|
|
272
|
+
e,
|
|
273
|
+
body.schemaVersion === REPORT_SCHEMA && body.kind === REPORT_KIND,
|
|
274
|
+
"report.schemaVersion/kind",
|
|
275
|
+
);
|
|
276
|
+
need(e, typeof body.ok === "boolean", "report.ok");
|
|
277
|
+
need(e, nullableText(body.toolVersion), "report.toolVersion");
|
|
278
|
+
need(e, record(body.inputs), "report.inputs");
|
|
279
|
+
if (body.ok === false) {
|
|
280
|
+
need(e, body.exitCode === 3 && text(body.error), "report.error/exitCode");
|
|
281
|
+
return e;
|
|
282
|
+
}
|
|
283
|
+
need(e, body.error === null, "report.error");
|
|
284
|
+
need(e, nullableText(body.dshVersion), "report.dshVersion");
|
|
285
|
+
e.push(...validateObservation(body.observation));
|
|
286
|
+
if (
|
|
287
|
+
record(body.observation) &&
|
|
288
|
+
body.observation.bound &&
|
|
289
|
+
body.observation.usable
|
|
290
|
+
) {
|
|
291
|
+
const o = body.observation;
|
|
292
|
+
if (text(body.dshVersion) && text(o.dshVersion))
|
|
293
|
+
need(
|
|
294
|
+
e,
|
|
295
|
+
o.dshVersion === body.dshVersion,
|
|
296
|
+
"observation/report.dshVersion",
|
|
297
|
+
);
|
|
298
|
+
if (text(body.inputs?.nodeModules) && text(o.profileId))
|
|
299
|
+
need(
|
|
300
|
+
e,
|
|
301
|
+
pathIdentity(o.profileId) === pathIdentity(body.inputs.nodeModules),
|
|
302
|
+
"observation/report.profileId",
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
e.push(...validateBaselineResult(body.baseline));
|
|
306
|
+
const summary = body.summary,
|
|
307
|
+
plugins = body.plugins;
|
|
308
|
+
if (!record(summary) || !record(plugins))
|
|
309
|
+
return [...e, "report.summary/plugins"];
|
|
310
|
+
for (const key of [...verdicts, "limited", "vacuousOk"])
|
|
311
|
+
need(e, integer(summary[key]), "summary." + key);
|
|
312
|
+
const counts = {
|
|
313
|
+
ok: 0,
|
|
314
|
+
partial: 0,
|
|
315
|
+
broken: 0,
|
|
316
|
+
unknown: 0,
|
|
317
|
+
limited: 0,
|
|
318
|
+
vacuousOk: 0,
|
|
319
|
+
};
|
|
320
|
+
for (const [name, p] of Object.entries(plugins)) {
|
|
321
|
+
if (!record(p)) {
|
|
322
|
+
e.push("plugin:" + name);
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
need(
|
|
326
|
+
e,
|
|
327
|
+
p.name === name &&
|
|
328
|
+
nullableText(p.version) &&
|
|
329
|
+
verdicts.includes(p.verdict),
|
|
330
|
+
"plugin.identity/verdict:" + name,
|
|
331
|
+
);
|
|
332
|
+
need(e, typeof p.vacuous === "boolean", "plugin.vacuous:" + name);
|
|
333
|
+
if (verdicts.includes(p.verdict)) counts[p.verdict]++;
|
|
334
|
+
if (p.vacuous && p.verdict === "ok") counts.vacuousOk++;
|
|
335
|
+
const c = p.coverage;
|
|
336
|
+
need(
|
|
337
|
+
e,
|
|
338
|
+
record(c) &&
|
|
339
|
+
["complete", "limited"].includes(c.status) &&
|
|
340
|
+
integer(c.verifiedCalls) &&
|
|
341
|
+
strings(c.limitations),
|
|
342
|
+
"plugin.coverage:" + name,
|
|
343
|
+
);
|
|
344
|
+
if (c?.status === "limited") counts.limited++;
|
|
345
|
+
for (const key of [
|
|
346
|
+
"findings",
|
|
347
|
+
"checked",
|
|
348
|
+
"clientSide",
|
|
349
|
+
"filtered",
|
|
350
|
+
"abstained",
|
|
351
|
+
"runtimeFacilities",
|
|
352
|
+
])
|
|
353
|
+
need(e, Array.isArray(p[key]), "plugin." + key + ":" + name);
|
|
354
|
+
if (Array.isArray(p.findings))
|
|
355
|
+
for (const f of p.findings)
|
|
356
|
+
e.push(...validateFinding(f).map((x) => name + ":" + x));
|
|
357
|
+
need(e, coherentVerdict(p), "plugin.verdict/findings:" + name);
|
|
358
|
+
if (record(c) && integer(c.verifiedCalls))
|
|
359
|
+
need(
|
|
360
|
+
e,
|
|
361
|
+
p.vacuous === (p.verdict === "ok" && c.verifiedCalls === 0),
|
|
362
|
+
"plugin.vacuous/coverage:" + name,
|
|
363
|
+
);
|
|
364
|
+
if (record(c) && strings(c.limitations) && Array.isArray(p.clientSide))
|
|
365
|
+
need(
|
|
366
|
+
e,
|
|
367
|
+
(c.status === "limited") ===
|
|
368
|
+
(c.limitations.length > 0 ||
|
|
369
|
+
p.clientSide.length > 0 ||
|
|
370
|
+
p.vacuous === true),
|
|
371
|
+
"plugin.coverage/status:" + name,
|
|
372
|
+
);
|
|
373
|
+
need(e, record(p.resolved), "plugin.resolved:" + name);
|
|
374
|
+
if (record(p.resolved))
|
|
375
|
+
for (const [service, value] of Object.entries(p.resolved))
|
|
376
|
+
need(
|
|
377
|
+
e,
|
|
378
|
+
checkedEntry(value) && value.service === service,
|
|
379
|
+
"plugin.resolved.entry:" + name + "." + service,
|
|
380
|
+
);
|
|
381
|
+
for (const [key, valid] of [
|
|
382
|
+
["checked", checkedEntry],
|
|
383
|
+
["clientSide", (v) => record(v) && text(v.service) && text(v.note)],
|
|
384
|
+
[
|
|
385
|
+
"filtered",
|
|
386
|
+
(v) => record(v) && text(v.service) && text(v.method) && text(v.reason),
|
|
387
|
+
],
|
|
388
|
+
["abstained", (v) => record(v) && text(v.service) && text(v.basis)],
|
|
389
|
+
["runtimeFacilities", (v) => record(v) && text(v.name) && text(v.reason)],
|
|
390
|
+
])
|
|
391
|
+
if (Array.isArray(p[key]))
|
|
392
|
+
need(e, p[key].every(valid), "plugin." + key + ".entry:" + name);
|
|
393
|
+
}
|
|
394
|
+
for (const key of Object.keys(counts))
|
|
395
|
+
need(e, counts[key] === summary[key], "summary.count:" + key);
|
|
396
|
+
need(e, body.exitCode === exitCodeForSummary(summary), "report.exitCode");
|
|
397
|
+
need(
|
|
398
|
+
e,
|
|
399
|
+
record(body.extraction) &&
|
|
400
|
+
typeof body.extraction.scanComplete === "boolean" &&
|
|
401
|
+
Array.isArray(body.extraction.diagnostics) &&
|
|
402
|
+
Array.isArray(body.extraction.manifests),
|
|
403
|
+
"report.extraction",
|
|
404
|
+
);
|
|
405
|
+
return e;
|
|
406
|
+
}
|
|
407
|
+
export function assertReportBody(body) {
|
|
408
|
+
const errors = validateReportBody(body);
|
|
409
|
+
if (errors.length) throw new Error("报告契约不匹配: " + errors.join(", "));
|
|
410
|
+
return body;
|
|
411
|
+
}
|
|
412
|
+
export function sealReportBody(input) {
|
|
413
|
+
const observation = input.observation || {};
|
|
414
|
+
const plugins = Object.fromEntries(
|
|
415
|
+
Object.entries(input.plugins || {}).map(([name, p]) => [
|
|
416
|
+
name,
|
|
417
|
+
{
|
|
418
|
+
...p,
|
|
419
|
+
findings: p.findings.map((f) => ({
|
|
420
|
+
...f,
|
|
421
|
+
location: f.location ?? null,
|
|
422
|
+
})),
|
|
423
|
+
},
|
|
424
|
+
]),
|
|
425
|
+
);
|
|
426
|
+
return assertReportBody({
|
|
427
|
+
...input,
|
|
428
|
+
schemaVersion: REPORT_SCHEMA,
|
|
429
|
+
kind: REPORT_KIND,
|
|
430
|
+
ok: true,
|
|
431
|
+
error: null,
|
|
432
|
+
exitCode: exitCodeForSummary(input.summary),
|
|
433
|
+
summary: { limited: 0, vacuousOk: 0, ...input.summary },
|
|
434
|
+
plugins,
|
|
435
|
+
observation: {
|
|
436
|
+
profileId: null,
|
|
437
|
+
dshVersion: null,
|
|
438
|
+
probedAt: null,
|
|
439
|
+
...observation,
|
|
440
|
+
},
|
|
441
|
+
baseline: input.baseline ?? null,
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
export function failureEnvelope(error, options = {}) {
|
|
445
|
+
const report = createFailure(error, {
|
|
446
|
+
toolVersion: options.toolVersion ?? null,
|
|
447
|
+
inputs: options.report?.inputs || {},
|
|
448
|
+
});
|
|
449
|
+
return {
|
|
450
|
+
...options,
|
|
451
|
+
schemaVersion: REPORT_SCHEMA,
|
|
452
|
+
kind: ENVELOPE_KIND,
|
|
453
|
+
ok: false,
|
|
454
|
+
error: report.error,
|
|
455
|
+
exitCode: 3,
|
|
456
|
+
toolVersion: report.toolVersion,
|
|
457
|
+
baseline: null,
|
|
458
|
+
report,
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
export function validateEnvelope(data) {
|
|
462
|
+
if (
|
|
463
|
+
!record(data) ||
|
|
464
|
+
data.schemaVersion !== REPORT_SCHEMA ||
|
|
465
|
+
data.kind !== ENVELOPE_KIND
|
|
466
|
+
)
|
|
467
|
+
return ["envelope.schemaVersion/kind"];
|
|
468
|
+
const e = validateReportBody(data.report);
|
|
469
|
+
need(
|
|
470
|
+
e,
|
|
471
|
+
typeof data.ok === "boolean" && data.ok === data.report?.ok,
|
|
472
|
+
"envelope.ok",
|
|
473
|
+
);
|
|
474
|
+
need(e, data.exitCode === data.report?.exitCode, "envelope.exitCode");
|
|
475
|
+
need(e, data.error === data.report?.error, "envelope.error");
|
|
476
|
+
need(
|
|
477
|
+
e,
|
|
478
|
+
data.toolVersion === data.report?.toolVersion,
|
|
479
|
+
"envelope.toolVersion",
|
|
480
|
+
);
|
|
481
|
+
need(
|
|
482
|
+
e,
|
|
483
|
+
JSON.stringify(data.baseline) ===
|
|
484
|
+
JSON.stringify(data.report?.baseline ?? null),
|
|
485
|
+
"envelope.baseline",
|
|
486
|
+
);
|
|
487
|
+
return e;
|
|
488
|
+
}
|
|
489
|
+
export function assertEnvelope(data) {
|
|
490
|
+
const errors = validateEnvelope(data);
|
|
491
|
+
if (errors.length) throw new Error("报告契约不匹配: " + errors.join(", "));
|
|
492
|
+
return data;
|
|
493
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import semver from "semver";
|
|
2
|
+
|
|
3
|
+
/** DSH app-boot 0.2.0-rc.2 checks every official dsh peer, including prereleases. */
|
|
4
|
+
export function declaredSupport(pkg, version) {
|
|
5
|
+
if (!version || !semver.valid(version)) return null;
|
|
6
|
+
const requirements = [];
|
|
7
|
+
for (const [name, range] of Object.entries(pkg.peerDependencies || {})) {
|
|
8
|
+
if (name === "@deepseek-ai/dsh" || name.startsWith("@deepseek-ai/dsh-"))
|
|
9
|
+
requirements.push({ name, range, source: "peerDependencies" });
|
|
10
|
+
}
|
|
11
|
+
if (typeof pkg.engines?.dsh === "string")
|
|
12
|
+
requirements.push({
|
|
13
|
+
name: "dsh",
|
|
14
|
+
range: pkg.engines.dsh,
|
|
15
|
+
source: "engines (advisory)",
|
|
16
|
+
});
|
|
17
|
+
const exact =
|
|
18
|
+
pkg.compatibility?.[version] ?? pkg.compatibility?.dshReleases?.[version];
|
|
19
|
+
let exactClaim =
|
|
20
|
+
exact === "compatible" ? true : exact === "incompatible" ? false : null;
|
|
21
|
+
const checks = requirements.map((r) => {
|
|
22
|
+
const range = ["workspace:^", "workspace:~", "workspace:*"].includes(
|
|
23
|
+
r.range,
|
|
24
|
+
)
|
|
25
|
+
? version
|
|
26
|
+
: r.range;
|
|
27
|
+
return {
|
|
28
|
+
...r,
|
|
29
|
+
valid: typeof range === "string" && semver.validRange(range) !== null,
|
|
30
|
+
compatible:
|
|
31
|
+
typeof range === "string" &&
|
|
32
|
+
range.trim() !== "" &&
|
|
33
|
+
semver.satisfies(version, range, { includePrerelease: true }),
|
|
34
|
+
};
|
|
35
|
+
});
|
|
36
|
+
if (!checks.length && exactClaim === null) return null;
|
|
37
|
+
return {
|
|
38
|
+
claimsCompatible: checks.length
|
|
39
|
+
? checks.every((r) => r.valid && r.compatible) && exactClaim !== false
|
|
40
|
+
: exactClaim,
|
|
41
|
+
raw: {
|
|
42
|
+
compatibility: pkg.compatibility ?? null,
|
|
43
|
+
engines: pkg.engines ?? null,
|
|
44
|
+
peers: checks,
|
|
45
|
+
},
|
|
46
|
+
notes: checks
|
|
47
|
+
.filter((r) => !r.valid)
|
|
48
|
+
.map((r) => `Invalid declared range for ${r.name}`),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { parentPort, workerData } from "node:worker_threads";
|
|
2
|
+
import { extractWithProvenance } from "./extract.mjs";
|
|
3
|
+
try {
|
|
4
|
+
parentPort.postMessage({
|
|
5
|
+
ok: true,
|
|
6
|
+
value: extractWithProvenance(workerData.nm, workerData.options),
|
|
7
|
+
});
|
|
8
|
+
} catch (error) {
|
|
9
|
+
parentPort.postMessage({ ok: false, error: error.message });
|
|
10
|
+
}
|