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,494 @@
|
|
|
1
|
+
// Generated from lib/contracts.mjs; run node plugin/scripts/stage-contract.mjs
|
|
2
|
+
/** Browser-safe runtime type contracts. No imports: staged into the prebuilt
|
|
3
|
+
* client and plugin host from this single source. Unknown additive fields are
|
|
4
|
+
* preserved. Version changes or missing evidence fail closed as tool errors.
|
|
5
|
+
* @typedef {{status:'complete'|'limited',verifiedCalls:number,limitations:string[]}} Coverage
|
|
6
|
+
* @typedef {{category:string,severity:'fatal'|'feature-dead'|'degraded',certainty:'certain'|'suspected',impactBasis:'measured'|'inferred',location:object|null}} Finding
|
|
7
|
+
* @typedef {{mode:'catalogue'|'runtime',bound:boolean,usable:boolean,notes:string[],profileId:string|null,dshVersion:string|null,probedAt:string|null}} Observation
|
|
8
|
+
*/
|
|
9
|
+
export const REPORT_SCHEMA = 1;
|
|
10
|
+
export const REPORT_KIND = "plugin-compat-check/result";
|
|
11
|
+
export const ENVELOPE_KIND = "plugin-compat-check/report";
|
|
12
|
+
const record = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
|
|
13
|
+
const text = (v) => typeof v === "string" && v.length > 0;
|
|
14
|
+
const nullableText = (v) => v === null || typeof v === "string";
|
|
15
|
+
const integer = (v) => Number.isSafeInteger(v) && v >= 0;
|
|
16
|
+
const strings = (v) =>
|
|
17
|
+
Array.isArray(v) && v.every((x) => typeof x === "string");
|
|
18
|
+
const verdicts = ["ok", "partial", "broken", "unknown"];
|
|
19
|
+
const severities = ["fatal", "feature-dead", "degraded"];
|
|
20
|
+
const certainties = ["certain", "suspected"];
|
|
21
|
+
const dispositions = [
|
|
22
|
+
"finding",
|
|
23
|
+
"client-side",
|
|
24
|
+
"self-provided",
|
|
25
|
+
"runtime-facility",
|
|
26
|
+
"filtered",
|
|
27
|
+
"abstained",
|
|
28
|
+
"guarded",
|
|
29
|
+
"no-calls",
|
|
30
|
+
"unknown",
|
|
31
|
+
];
|
|
32
|
+
function coherentVerdict(p) {
|
|
33
|
+
if (!Array.isArray(p.findings)) return false;
|
|
34
|
+
return (
|
|
35
|
+
p.verdict === "unknown" ||
|
|
36
|
+
p.verdict ===
|
|
37
|
+
(p.findings.some((f) => f?.severity === "fatal")
|
|
38
|
+
? "broken"
|
|
39
|
+
: p.findings.length
|
|
40
|
+
? "partial"
|
|
41
|
+
: "ok")
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
function snapshotFinding(f) {
|
|
45
|
+
return (
|
|
46
|
+
record(f) &&
|
|
47
|
+
text(f.key) &&
|
|
48
|
+
text(f.category) &&
|
|
49
|
+
severities.includes(f.severity) &&
|
|
50
|
+
certainties.includes(f.certainty) &&
|
|
51
|
+
nullableText(f.location)
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
function checkedEntry(v) {
|
|
55
|
+
return record(v) && text(v.service) && dispositions.includes(v.disposition);
|
|
56
|
+
}
|
|
57
|
+
// Producers resolve relative observation paths before serialization. Lexical
|
|
58
|
+
// normalization here is platform independent and needs no browser Node shim.
|
|
59
|
+
function pathIdentity(v) {
|
|
60
|
+
const normalized = v.replace(/\\/g, "/");
|
|
61
|
+
const parts = [];
|
|
62
|
+
for (const part of normalized.split("/")) {
|
|
63
|
+
if (part === "..") parts.pop();
|
|
64
|
+
else if (part && part !== ".") parts.push(part);
|
|
65
|
+
}
|
|
66
|
+
return (normalized.startsWith("/") ? "/" : "") + parts.join("/");
|
|
67
|
+
}
|
|
68
|
+
function need(errors, condition, path) {
|
|
69
|
+
if (!condition) errors.push(path);
|
|
70
|
+
}
|
|
71
|
+
export function exitCodeForSummary(summary) {
|
|
72
|
+
return summary.broken > 0
|
|
73
|
+
? 2
|
|
74
|
+
: summary.partial > 0 || summary.unknown > 0
|
|
75
|
+
? 1
|
|
76
|
+
: 0;
|
|
77
|
+
}
|
|
78
|
+
export function createFailure(error, options = {}) {
|
|
79
|
+
return {
|
|
80
|
+
...options,
|
|
81
|
+
schemaVersion: REPORT_SCHEMA,
|
|
82
|
+
kind: REPORT_KIND,
|
|
83
|
+
ok: false,
|
|
84
|
+
exitCode: 3,
|
|
85
|
+
toolVersion: options.toolVersion ?? null,
|
|
86
|
+
error: String(error || "Unknown tool error"),
|
|
87
|
+
inputs: options.inputs || {},
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
export function validateObservation(value) {
|
|
91
|
+
const e = [];
|
|
92
|
+
if (!record(value)) return ["observation"];
|
|
93
|
+
need(e, ["runtime", "catalogue"].includes(value.mode), "observation.mode");
|
|
94
|
+
need(e, typeof value.bound === "boolean", "observation.bound");
|
|
95
|
+
need(e, typeof value.usable === "boolean", "observation.usable");
|
|
96
|
+
need(e, strings(value.notes), "observation.notes");
|
|
97
|
+
for (const key of ["profileId", "dshVersion", "probedAt"])
|
|
98
|
+
need(e, nullableText(value[key]), "observation." + key);
|
|
99
|
+
if (value.probedAt !== null)
|
|
100
|
+
need(
|
|
101
|
+
e,
|
|
102
|
+
Number.isFinite(Date.parse(value.probedAt)),
|
|
103
|
+
"observation.probedAt",
|
|
104
|
+
);
|
|
105
|
+
need(
|
|
106
|
+
e,
|
|
107
|
+
(value.mode === "runtime") === value.usable,
|
|
108
|
+
"observation.mode/usable",
|
|
109
|
+
);
|
|
110
|
+
return e;
|
|
111
|
+
}
|
|
112
|
+
export function validateFinding(f) {
|
|
113
|
+
const e = [];
|
|
114
|
+
if (!record(f)) return ["finding"];
|
|
115
|
+
need(e, text(f.category), "finding.category");
|
|
116
|
+
need(e, severities.includes(f.severity), "finding.severity");
|
|
117
|
+
need(e, certainties.includes(f.certainty), "finding.certainty");
|
|
118
|
+
need(
|
|
119
|
+
e,
|
|
120
|
+
["inferred", "measured"].includes(f.impactBasis),
|
|
121
|
+
"finding.impactBasis",
|
|
122
|
+
);
|
|
123
|
+
need(e, text(f.impact), "finding.impact");
|
|
124
|
+
need(e, text(f.basis), "finding.basis");
|
|
125
|
+
for (const key of ["service", "method"])
|
|
126
|
+
if (f[key] !== undefined) need(e, nullableText(f[key]), "finding." + key);
|
|
127
|
+
need(
|
|
128
|
+
e,
|
|
129
|
+
f.location === null ||
|
|
130
|
+
(record(f.location) &&
|
|
131
|
+
text(f.location.file) &&
|
|
132
|
+
Number.isSafeInteger(f.location.line) &&
|
|
133
|
+
f.location.line > 0),
|
|
134
|
+
"finding.location",
|
|
135
|
+
);
|
|
136
|
+
if (f.evidence !== undefined) need(e, record(f.evidence), "finding.evidence");
|
|
137
|
+
if (record(f.location)) {
|
|
138
|
+
for (const key of ["column", "start", "end"])
|
|
139
|
+
if (f.location[key] !== undefined && f.location[key] !== null)
|
|
140
|
+
need(e, integer(f.location[key]), "finding.location." + key);
|
|
141
|
+
if (integer(f.location.start) && integer(f.location.end))
|
|
142
|
+
need(e, f.location.end >= f.location.start, "finding.location.span");
|
|
143
|
+
}
|
|
144
|
+
return e;
|
|
145
|
+
}
|
|
146
|
+
export function validateBaseline(value) {
|
|
147
|
+
const e = [];
|
|
148
|
+
if (!record(value) || value.schemaVersion !== 2)
|
|
149
|
+
return ["baseline.schemaVersion"];
|
|
150
|
+
need(e, nullableText(value.dshVersion), "baseline.dshVersion");
|
|
151
|
+
need(
|
|
152
|
+
e,
|
|
153
|
+
record(value.identity) && text(value.identity.mode),
|
|
154
|
+
"baseline.identity",
|
|
155
|
+
);
|
|
156
|
+
if (!record(value.plugins)) return [...e, "baseline.plugins"];
|
|
157
|
+
for (const [name, p] of Object.entries(value.plugins)) {
|
|
158
|
+
if (!record(p)) {
|
|
159
|
+
e.push("baseline.plugin:" + name);
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
need(
|
|
163
|
+
e,
|
|
164
|
+
nullableText(p.version) && verdicts.includes(p.verdict),
|
|
165
|
+
"baseline.plugin:" + name,
|
|
166
|
+
);
|
|
167
|
+
need(e, Array.isArray(p.findings), "baseline.findings:" + name);
|
|
168
|
+
need(e, coherentVerdict(p), "baseline.verdict/findings:" + name);
|
|
169
|
+
if (p.vacuous !== undefined)
|
|
170
|
+
need(e, typeof p.vacuous === "boolean", "baseline.vacuous:" + name);
|
|
171
|
+
for (const f of Array.isArray(p.findings) ? p.findings : []) {
|
|
172
|
+
if (!record(f)) {
|
|
173
|
+
e.push("baseline.finding:" + name);
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
need(e, snapshotFinding(f), "baseline.finding:" + name);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return e;
|
|
180
|
+
}
|
|
181
|
+
function validateBaselineResult(b) {
|
|
182
|
+
if (b === null) return [];
|
|
183
|
+
const e = [];
|
|
184
|
+
if (!record(b)) return ["baseline result"];
|
|
185
|
+
if (b.error !== undefined) {
|
|
186
|
+
need(e, text(b.error) && text(b.note), "baseline.error");
|
|
187
|
+
return e;
|
|
188
|
+
}
|
|
189
|
+
need(e, typeof b.wrote === "boolean", "baseline.wrote");
|
|
190
|
+
need(e, record(b.badge), "baseline.badge");
|
|
191
|
+
if (record(b.badge)) {
|
|
192
|
+
need(
|
|
193
|
+
e,
|
|
194
|
+
typeof b.badge.show === "boolean" &&
|
|
195
|
+
nullableText(b.badge.text) &&
|
|
196
|
+
nullableText(b.badge.detail) &&
|
|
197
|
+
[null, "worse", "better", "mixed", "neutral"].includes(
|
|
198
|
+
b.badge.direction,
|
|
199
|
+
),
|
|
200
|
+
"baseline.badge.fields",
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
if (b.diff !== null) {
|
|
204
|
+
need(
|
|
205
|
+
e,
|
|
206
|
+
record(b.diff) && typeof b.diff.comparable === "boolean",
|
|
207
|
+
"baseline.diff",
|
|
208
|
+
);
|
|
209
|
+
if (record(b.diff)) {
|
|
210
|
+
for (const key of [
|
|
211
|
+
"added",
|
|
212
|
+
"removed",
|
|
213
|
+
"worsened",
|
|
214
|
+
"improved",
|
|
215
|
+
"updated",
|
|
216
|
+
]) {
|
|
217
|
+
need(e, Array.isArray(b.diff[key]), "baseline.diff." + key);
|
|
218
|
+
for (const entry of Array.isArray(b.diff[key]) ? b.diff[key] : []) {
|
|
219
|
+
const at = "baseline.diff." + key;
|
|
220
|
+
if (!record(entry)) {
|
|
221
|
+
e.push(at + ".entry");
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
need(e, text(entry.name), at + ".name");
|
|
225
|
+
if (key === "added" || key === "removed") {
|
|
226
|
+
need(e, nullableText(entry.version), at + ".version");
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
need(
|
|
230
|
+
e,
|
|
231
|
+
nullableText(entry.versionFrom) &&
|
|
232
|
+
nullableText(entry.versionTo) &&
|
|
233
|
+
typeof entry.versionChanged === "boolean" &&
|
|
234
|
+
verdicts.includes(entry.verdictFrom) &&
|
|
235
|
+
verdicts.includes(entry.verdictTo) &&
|
|
236
|
+
text(entry.reason),
|
|
237
|
+
at + ".fields",
|
|
238
|
+
);
|
|
239
|
+
for (const field of ["newFindings", "goneFindings"]) {
|
|
240
|
+
need(
|
|
241
|
+
e,
|
|
242
|
+
Array.isArray(entry[field]) &&
|
|
243
|
+
entry[field].every(snapshotFinding),
|
|
244
|
+
at + "." + field,
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
need(
|
|
248
|
+
e,
|
|
249
|
+
Array.isArray(entry.changedFindings) &&
|
|
250
|
+
entry.changedFindings.every(
|
|
251
|
+
(c) =>
|
|
252
|
+
record(c) && snapshotFinding(c.from) && snapshotFinding(c.to),
|
|
253
|
+
),
|
|
254
|
+
at + ".changedFindings",
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
for (const key of ["dshVersionFrom", "dshVersionTo", "reason"])
|
|
259
|
+
need(e, nullableText(b.diff[key]), "baseline.diff." + key);
|
|
260
|
+
need(
|
|
261
|
+
e,
|
|
262
|
+
typeof b.diff.dshVersionChanged === "boolean",
|
|
263
|
+
"baseline.diff.dshVersionChanged",
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return e;
|
|
268
|
+
}
|
|
269
|
+
export function validateReportBody(body) {
|
|
270
|
+
const e = [];
|
|
271
|
+
if (!record(body)) return ["report"];
|
|
272
|
+
need(
|
|
273
|
+
e,
|
|
274
|
+
body.schemaVersion === REPORT_SCHEMA && body.kind === REPORT_KIND,
|
|
275
|
+
"report.schemaVersion/kind",
|
|
276
|
+
);
|
|
277
|
+
need(e, typeof body.ok === "boolean", "report.ok");
|
|
278
|
+
need(e, nullableText(body.toolVersion), "report.toolVersion");
|
|
279
|
+
need(e, record(body.inputs), "report.inputs");
|
|
280
|
+
if (body.ok === false) {
|
|
281
|
+
need(e, body.exitCode === 3 && text(body.error), "report.error/exitCode");
|
|
282
|
+
return e;
|
|
283
|
+
}
|
|
284
|
+
need(e, body.error === null, "report.error");
|
|
285
|
+
need(e, nullableText(body.dshVersion), "report.dshVersion");
|
|
286
|
+
e.push(...validateObservation(body.observation));
|
|
287
|
+
if (
|
|
288
|
+
record(body.observation) &&
|
|
289
|
+
body.observation.bound &&
|
|
290
|
+
body.observation.usable
|
|
291
|
+
) {
|
|
292
|
+
const o = body.observation;
|
|
293
|
+
if (text(body.dshVersion) && text(o.dshVersion))
|
|
294
|
+
need(
|
|
295
|
+
e,
|
|
296
|
+
o.dshVersion === body.dshVersion,
|
|
297
|
+
"observation/report.dshVersion",
|
|
298
|
+
);
|
|
299
|
+
if (text(body.inputs?.nodeModules) && text(o.profileId))
|
|
300
|
+
need(
|
|
301
|
+
e,
|
|
302
|
+
pathIdentity(o.profileId) === pathIdentity(body.inputs.nodeModules),
|
|
303
|
+
"observation/report.profileId",
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
e.push(...validateBaselineResult(body.baseline));
|
|
307
|
+
const summary = body.summary,
|
|
308
|
+
plugins = body.plugins;
|
|
309
|
+
if (!record(summary) || !record(plugins))
|
|
310
|
+
return [...e, "report.summary/plugins"];
|
|
311
|
+
for (const key of [...verdicts, "limited", "vacuousOk"])
|
|
312
|
+
need(e, integer(summary[key]), "summary." + key);
|
|
313
|
+
const counts = {
|
|
314
|
+
ok: 0,
|
|
315
|
+
partial: 0,
|
|
316
|
+
broken: 0,
|
|
317
|
+
unknown: 0,
|
|
318
|
+
limited: 0,
|
|
319
|
+
vacuousOk: 0,
|
|
320
|
+
};
|
|
321
|
+
for (const [name, p] of Object.entries(plugins)) {
|
|
322
|
+
if (!record(p)) {
|
|
323
|
+
e.push("plugin:" + name);
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
need(
|
|
327
|
+
e,
|
|
328
|
+
p.name === name &&
|
|
329
|
+
nullableText(p.version) &&
|
|
330
|
+
verdicts.includes(p.verdict),
|
|
331
|
+
"plugin.identity/verdict:" + name,
|
|
332
|
+
);
|
|
333
|
+
need(e, typeof p.vacuous === "boolean", "plugin.vacuous:" + name);
|
|
334
|
+
if (verdicts.includes(p.verdict)) counts[p.verdict]++;
|
|
335
|
+
if (p.vacuous && p.verdict === "ok") counts.vacuousOk++;
|
|
336
|
+
const c = p.coverage;
|
|
337
|
+
need(
|
|
338
|
+
e,
|
|
339
|
+
record(c) &&
|
|
340
|
+
["complete", "limited"].includes(c.status) &&
|
|
341
|
+
integer(c.verifiedCalls) &&
|
|
342
|
+
strings(c.limitations),
|
|
343
|
+
"plugin.coverage:" + name,
|
|
344
|
+
);
|
|
345
|
+
if (c?.status === "limited") counts.limited++;
|
|
346
|
+
for (const key of [
|
|
347
|
+
"findings",
|
|
348
|
+
"checked",
|
|
349
|
+
"clientSide",
|
|
350
|
+
"filtered",
|
|
351
|
+
"abstained",
|
|
352
|
+
"runtimeFacilities",
|
|
353
|
+
])
|
|
354
|
+
need(e, Array.isArray(p[key]), "plugin." + key + ":" + name);
|
|
355
|
+
if (Array.isArray(p.findings))
|
|
356
|
+
for (const f of p.findings)
|
|
357
|
+
e.push(...validateFinding(f).map((x) => name + ":" + x));
|
|
358
|
+
need(e, coherentVerdict(p), "plugin.verdict/findings:" + name);
|
|
359
|
+
if (record(c) && integer(c.verifiedCalls))
|
|
360
|
+
need(
|
|
361
|
+
e,
|
|
362
|
+
p.vacuous === (p.verdict === "ok" && c.verifiedCalls === 0),
|
|
363
|
+
"plugin.vacuous/coverage:" + name,
|
|
364
|
+
);
|
|
365
|
+
if (record(c) && strings(c.limitations) && Array.isArray(p.clientSide))
|
|
366
|
+
need(
|
|
367
|
+
e,
|
|
368
|
+
(c.status === "limited") ===
|
|
369
|
+
(c.limitations.length > 0 ||
|
|
370
|
+
p.clientSide.length > 0 ||
|
|
371
|
+
p.vacuous === true),
|
|
372
|
+
"plugin.coverage/status:" + name,
|
|
373
|
+
);
|
|
374
|
+
need(e, record(p.resolved), "plugin.resolved:" + name);
|
|
375
|
+
if (record(p.resolved))
|
|
376
|
+
for (const [service, value] of Object.entries(p.resolved))
|
|
377
|
+
need(
|
|
378
|
+
e,
|
|
379
|
+
checkedEntry(value) && value.service === service,
|
|
380
|
+
"plugin.resolved.entry:" + name + "." + service,
|
|
381
|
+
);
|
|
382
|
+
for (const [key, valid] of [
|
|
383
|
+
["checked", checkedEntry],
|
|
384
|
+
["clientSide", (v) => record(v) && text(v.service) && text(v.note)],
|
|
385
|
+
[
|
|
386
|
+
"filtered",
|
|
387
|
+
(v) => record(v) && text(v.service) && text(v.method) && text(v.reason),
|
|
388
|
+
],
|
|
389
|
+
["abstained", (v) => record(v) && text(v.service) && text(v.basis)],
|
|
390
|
+
["runtimeFacilities", (v) => record(v) && text(v.name) && text(v.reason)],
|
|
391
|
+
])
|
|
392
|
+
if (Array.isArray(p[key]))
|
|
393
|
+
need(e, p[key].every(valid), "plugin." + key + ".entry:" + name);
|
|
394
|
+
}
|
|
395
|
+
for (const key of Object.keys(counts))
|
|
396
|
+
need(e, counts[key] === summary[key], "summary.count:" + key);
|
|
397
|
+
need(e, body.exitCode === exitCodeForSummary(summary), "report.exitCode");
|
|
398
|
+
need(
|
|
399
|
+
e,
|
|
400
|
+
record(body.extraction) &&
|
|
401
|
+
typeof body.extraction.scanComplete === "boolean" &&
|
|
402
|
+
Array.isArray(body.extraction.diagnostics) &&
|
|
403
|
+
Array.isArray(body.extraction.manifests),
|
|
404
|
+
"report.extraction",
|
|
405
|
+
);
|
|
406
|
+
return e;
|
|
407
|
+
}
|
|
408
|
+
export function assertReportBody(body) {
|
|
409
|
+
const errors = validateReportBody(body);
|
|
410
|
+
if (errors.length) throw new Error("报告契约不匹配: " + errors.join(", "));
|
|
411
|
+
return body;
|
|
412
|
+
}
|
|
413
|
+
export function sealReportBody(input) {
|
|
414
|
+
const observation = input.observation || {};
|
|
415
|
+
const plugins = Object.fromEntries(
|
|
416
|
+
Object.entries(input.plugins || {}).map(([name, p]) => [
|
|
417
|
+
name,
|
|
418
|
+
{
|
|
419
|
+
...p,
|
|
420
|
+
findings: p.findings.map((f) => ({
|
|
421
|
+
...f,
|
|
422
|
+
location: f.location ?? null,
|
|
423
|
+
})),
|
|
424
|
+
},
|
|
425
|
+
]),
|
|
426
|
+
);
|
|
427
|
+
return assertReportBody({
|
|
428
|
+
...input,
|
|
429
|
+
schemaVersion: REPORT_SCHEMA,
|
|
430
|
+
kind: REPORT_KIND,
|
|
431
|
+
ok: true,
|
|
432
|
+
error: null,
|
|
433
|
+
exitCode: exitCodeForSummary(input.summary),
|
|
434
|
+
summary: { limited: 0, vacuousOk: 0, ...input.summary },
|
|
435
|
+
plugins,
|
|
436
|
+
observation: {
|
|
437
|
+
profileId: null,
|
|
438
|
+
dshVersion: null,
|
|
439
|
+
probedAt: null,
|
|
440
|
+
...observation,
|
|
441
|
+
},
|
|
442
|
+
baseline: input.baseline ?? null,
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
export function failureEnvelope(error, options = {}) {
|
|
446
|
+
const report = createFailure(error, {
|
|
447
|
+
toolVersion: options.toolVersion ?? null,
|
|
448
|
+
inputs: options.report?.inputs || {},
|
|
449
|
+
});
|
|
450
|
+
return {
|
|
451
|
+
...options,
|
|
452
|
+
schemaVersion: REPORT_SCHEMA,
|
|
453
|
+
kind: ENVELOPE_KIND,
|
|
454
|
+
ok: false,
|
|
455
|
+
error: report.error,
|
|
456
|
+
exitCode: 3,
|
|
457
|
+
toolVersion: report.toolVersion,
|
|
458
|
+
baseline: null,
|
|
459
|
+
report,
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
export function validateEnvelope(data) {
|
|
463
|
+
if (
|
|
464
|
+
!record(data) ||
|
|
465
|
+
data.schemaVersion !== REPORT_SCHEMA ||
|
|
466
|
+
data.kind !== ENVELOPE_KIND
|
|
467
|
+
)
|
|
468
|
+
return ["envelope.schemaVersion/kind"];
|
|
469
|
+
const e = validateReportBody(data.report);
|
|
470
|
+
need(
|
|
471
|
+
e,
|
|
472
|
+
typeof data.ok === "boolean" && data.ok === data.report?.ok,
|
|
473
|
+
"envelope.ok",
|
|
474
|
+
);
|
|
475
|
+
need(e, data.exitCode === data.report?.exitCode, "envelope.exitCode");
|
|
476
|
+
need(e, data.error === data.report?.error, "envelope.error");
|
|
477
|
+
need(
|
|
478
|
+
e,
|
|
479
|
+
data.toolVersion === data.report?.toolVersion,
|
|
480
|
+
"envelope.toolVersion",
|
|
481
|
+
);
|
|
482
|
+
need(
|
|
483
|
+
e,
|
|
484
|
+
JSON.stringify(data.baseline) ===
|
|
485
|
+
JSON.stringify(data.report?.baseline ?? null),
|
|
486
|
+
"envelope.baseline",
|
|
487
|
+
);
|
|
488
|
+
return e;
|
|
489
|
+
}
|
|
490
|
+
export function assertEnvelope(data) {
|
|
491
|
+
const errors = validateEnvelope(data);
|
|
492
|
+
if (errors.length) throw new Error("报告契约不匹配: " + errors.join(", "));
|
|
493
|
+
return data;
|
|
494
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin-compat-check",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"description": "Checks installed DSH plugins against the current DSH version, with source locations, runtime evidence and coverage limits. Includes a compatibility panel and agent tool.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "index.js",
|
|
@@ -28,23 +28,28 @@
|
|
|
28
28
|
"lib/",
|
|
29
29
|
"cordis.patch.yml",
|
|
30
30
|
"kernel/",
|
|
31
|
-
"scripts/"
|
|
31
|
+
"scripts/",
|
|
32
|
+
"LICENSE"
|
|
32
33
|
],
|
|
33
34
|
"engines": {
|
|
34
35
|
"node": "^22.19.0 || >=24"
|
|
35
36
|
},
|
|
36
37
|
"scripts": {
|
|
37
|
-
"prepack": "node scripts/stage-skill.mjs && node scripts/stage-kernel.mjs",
|
|
38
|
+
"prepack": "node scripts/stage-skill.mjs && node scripts/stage-contract.mjs && node scripts/stage-kernel.mjs",
|
|
38
39
|
"postpack": "node scripts/unstage-kernel.mjs",
|
|
39
40
|
"prepublishOnly": "node scripts/prepublish-check.mjs",
|
|
40
41
|
"stage-skill": "node scripts/stage-skill.mjs"
|
|
41
42
|
},
|
|
42
43
|
"keywords": [
|
|
43
44
|
"dsh",
|
|
45
|
+
"deepseek",
|
|
44
46
|
"deepseek-harness",
|
|
47
|
+
"dsh-plugin",
|
|
45
48
|
"plugin",
|
|
46
49
|
"compatibility",
|
|
47
|
-
"health-check"
|
|
50
|
+
"health-check",
|
|
51
|
+
"cordis",
|
|
52
|
+
"agent-skills"
|
|
48
53
|
],
|
|
49
54
|
"repository": {
|
|
50
55
|
"type": "git",
|
|
@@ -53,5 +58,9 @@
|
|
|
53
58
|
"homepage": "https://github.com/lcy0121/dsh-plugin-compat-check#readme",
|
|
54
59
|
"bugs": {
|
|
55
60
|
"url": "https://github.com/lcy0121/dsh-plugin-compat-check/issues"
|
|
61
|
+
},
|
|
62
|
+
"dependencies": {
|
|
63
|
+
"@babel/parser": "7.29.9",
|
|
64
|
+
"semver": "7.7.3"
|
|
56
65
|
}
|
|
57
66
|
}
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* ① **版本号与内核不一致**。版本的真源是 `lib/version.mjs`,但 `package.json`
|
|
10
10
|
* 只能写字面量(npm 只认字面量)。改了一处忘了另一处,发布出去的就是
|
|
11
11
|
* 「包的版本是 A、它自己 `--version` 报 B」—— 而这恰恰是这次发布的**主题**
|
|
12
|
-
* (见 Plan.md §
|
|
12
|
+
* (见 docs/Plan.md §1、§6:「单版本」)。
|
|
13
13
|
*
|
|
14
14
|
* ② **测试没过就发**。`npm publish` 默认**不跑测试**。测试没过的包发出去,
|
|
15
15
|
* 用户拿到的是我们已知有问题的版本,而他还以为那是"刚发布的最新版"。
|
|
@@ -24,8 +24,9 @@
|
|
|
24
24
|
|
|
25
25
|
import { execFileSync } from 'node:child_process';
|
|
26
26
|
import fs from 'node:fs';
|
|
27
|
+
import os from 'node:os';
|
|
27
28
|
import path from 'node:path';
|
|
28
|
-
import { fileURLToPath } from 'node:url';
|
|
29
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
29
30
|
|
|
30
31
|
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
31
32
|
const PLUGIN = path.resolve(HERE, '..');
|
|
@@ -46,7 +47,7 @@ if (!fs.existsSync(path.join(REPO, 'lib', 'version.mjs'))) {
|
|
|
46
47
|
}
|
|
47
48
|
|
|
48
49
|
// ── ② 版本一致 ──
|
|
49
|
-
const kernel = await import(
|
|
50
|
+
const kernel = await import(pathToFileURL(path.join(REPO,'lib','version.mjs')).href);
|
|
50
51
|
const pkg = JSON.parse(fs.readFileSync(path.join(PLUGIN, 'package.json'), 'utf8'));
|
|
51
52
|
if (pkg.version !== kernel.TOOL_VERSION) {
|
|
52
53
|
fail([
|
|
@@ -63,22 +64,26 @@ if (pkg.version !== kernel.TOOL_VERSION) {
|
|
|
63
64
|
if (pkg.private === true) {
|
|
64
65
|
fail([
|
|
65
66
|
'plugin/package.json 里还写着 `"private": true`,npm 会拒绝发布。',
|
|
66
|
-
' 要去掉它 —— 但**先确认版本号已经定下来**(见 Plan.md §
|
|
67
|
+
' 要去掉它 —— 但**先确认版本号已经定下来**(见 docs/Plan.md §1、§6)。',
|
|
67
68
|
]);
|
|
68
69
|
}
|
|
69
70
|
|
|
70
71
|
// ── ④ 测试必须全过 ──
|
|
72
|
+
const summaryDir=fs.mkdtempSync(path.join(os.tmpdir(),'dsc-release-'));
|
|
71
73
|
try {
|
|
72
|
-
execFileSync(process.execPath, ['
|
|
73
|
-
cwd: REPO, stdio: ['ignore', 'pipe', 'pipe'], timeout: 600000,
|
|
74
|
+
const summary=execFileSync(process.execPath, ['tools/run-tests.mjs','--test-reporter=tap'], {
|
|
75
|
+
cwd: REPO, stdio: ['ignore', 'pipe', 'pipe'], timeout: 600000, maxBuffer:16*1024*1024,env:{...process.env,DSH_HOME:path.join(summaryDir,'state')},
|
|
74
76
|
});
|
|
77
|
+
const file=path.join(summaryDir,'tests.tap');fs.writeFileSync(file,summary);
|
|
78
|
+
execFileSync(process.execPath,['tools/doc-check.mjs','--test-summary',file],{cwd:REPO,stdio:'pipe'});
|
|
75
79
|
} catch (error) {
|
|
76
80
|
const out = `${error.stdout ?? ''}${error.stderr ?? ''}`;
|
|
77
|
-
|
|
78
|
-
fail(['
|
|
79
|
-
' 跑 `
|
|
81
|
+
fs.rmSync(summaryDir,{recursive:true,force:true});
|
|
82
|
+
fail(['仓库的测试或文档检查没有全过:', '', out.trim() || error.message || String(error), '',
|
|
83
|
+
' 跑 `npm test` 看全部输出。']);
|
|
80
84
|
}
|
|
81
85
|
|
|
86
|
+
fs.rmSync(summaryDir,{recursive:true,force:true});
|
|
82
87
|
process.stdout.write(
|
|
83
88
|
`✓ 发布前检查通过:version=${pkg.version},测试全过\n`,
|
|
84
89
|
);
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/** Keep host/browser contract artifacts byte-reproducible from one pure module. */
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
const plugin = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."),
|
|
7
|
+
root = path.dirname(plugin);
|
|
8
|
+
const source = fs.readFileSync(path.join(root, "lib/contracts.mjs"), "utf8");
|
|
9
|
+
if (/^import\s/m.test(source))
|
|
10
|
+
throw new Error("Browser contract source must not import Node dependencies");
|
|
11
|
+
const start = " // BEGIN GENERATED REPORT CONTRACT",
|
|
12
|
+
end = " // END GENERATED REPORT CONTRACT";
|
|
13
|
+
const client = path.join(plugin, "lib/client.js"),
|
|
14
|
+
old = fs.readFileSync(client, "utf8");
|
|
15
|
+
const block =
|
|
16
|
+
start +
|
|
17
|
+
"\n var ReportContract = (function () {\n" +
|
|
18
|
+
source.replace(/^export /gm, "") +
|
|
19
|
+
"\nreturn { assertEnvelope: assertEnvelope };\n })();\n" +
|
|
20
|
+
end;
|
|
21
|
+
const a = old.indexOf(start),
|
|
22
|
+
b = old.indexOf(end);
|
|
23
|
+
let next;
|
|
24
|
+
if (a >= 0 && b > a) next = old.slice(0, a) + block + old.slice(b + end.length);
|
|
25
|
+
else {
|
|
26
|
+
const marker = ' var React = require("react");';
|
|
27
|
+
if (!old.includes(marker)) throw new Error("Missing client injection marker");
|
|
28
|
+
next = old.replace(marker, block + "\n\n" + marker);
|
|
29
|
+
}
|
|
30
|
+
const host = path.join(plugin, "lib/report-contract.js"),
|
|
31
|
+
header =
|
|
32
|
+
"// Generated from lib/contracts.mjs; run node plugin/scripts/stage-contract.mjs\n";
|
|
33
|
+
for (const [file, value] of [
|
|
34
|
+
[client, next],
|
|
35
|
+
[host, header + source],
|
|
36
|
+
]) {
|
|
37
|
+
if (fs.existsSync(file) && fs.readFileSync(file, "utf8") === value) continue;
|
|
38
|
+
if (process.argv.includes("--check"))
|
|
39
|
+
throw new Error(
|
|
40
|
+
"Stale generated report contract: " + path.relative(root, file),
|
|
41
|
+
);
|
|
42
|
+
fs.writeFileSync(file, value);
|
|
43
|
+
}
|