dsh-plugin-compat-check 0.1.3 → 0.1.5

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.
Files changed (45) hide show
  1. package/README.md +36 -20
  2. package/THIRD_PARTY_NOTICES.md +139 -0
  3. package/index.js +288 -55
  4. package/kernel/lib/author-ci-output.mjs +188 -0
  5. package/kernel/lib/author-ci-policy.mjs +285 -0
  6. package/kernel/lib/catalog.mjs +38 -0
  7. package/kernel/lib/contracts.mjs +88 -0
  8. package/kernel/lib/dependency-graph.mjs +650 -0
  9. package/kernel/lib/extract.mjs +14 -0
  10. package/kernel/lib/finding-guidance.mjs +72 -0
  11. package/kernel/lib/finding-review-store.mjs +245 -0
  12. package/kernel/lib/finding-review.mjs +338 -0
  13. package/kernel/lib/functional-health-contract.mjs +337 -0
  14. package/kernel/lib/issue-report.mjs +217 -0
  15. package/kernel/lib/observation.mjs +19 -9
  16. package/kernel/lib/owned-process.mjs +287 -0
  17. package/kernel/lib/report-identity-worker.mjs +153 -0
  18. package/kernel/lib/report-identity.mjs +15 -0
  19. package/kernel/lib/report-service.mjs +196 -11
  20. package/kernel/lib/report.mjs +76 -11
  21. package/kernel/lib/source-analysis.mjs +24 -6
  22. package/kernel/lib/upgrade-capture-worker.mjs +20 -0
  23. package/kernel/lib/upgrade-cleanup-worker.mjs +28 -0
  24. package/kernel/lib/upgrade-contract.mjs +471 -0
  25. package/kernel/lib/upgrade-input.mjs +309 -0
  26. package/kernel/lib/upgrade-preview.mjs +256 -0
  27. package/kernel/lib/upgrade-worker.mjs +50 -0
  28. package/kernel/lib/verdict.mjs +8 -1
  29. package/kernel/lib/version.mjs +1 -1
  30. package/kernel/lib/worker-analysis.mjs +16 -1
  31. package/kernel/lib/workflow-panels.mjs +706 -0
  32. package/kernel/rules/README.md +8 -6
  33. package/lib/client.js +2974 -69
  34. package/lib/dependency-graph.js +651 -0
  35. package/lib/finding-guidance.js +73 -0
  36. package/lib/finding-review.js +339 -0
  37. package/lib/functional-health-contract.js +338 -0
  38. package/lib/report-contract.js +88 -0
  39. package/lib/upgrade-contract.js +472 -0
  40. package/lib/workflow-panels.js +707 -0
  41. package/package.json +5 -3
  42. package/scripts/prepublish-check.mjs +28 -13
  43. package/scripts/stage-contract.mjs +91 -29
  44. package/scripts/stage-kernel.mjs +4 -3
  45. package/skill/SKILL.md +92 -39
@@ -0,0 +1,471 @@
1
+ /** Browser-safe preview contract, also staged into the plugin. No imports. */
2
+ export const SUPPORTED_PREVIEW_VERSIONS = Object.freeze([
3
+ "0.2.0-rc.2",
4
+ "0.2.1-rc.2",
5
+ ]);
6
+ const record = (x) => x !== null && typeof x === "object" && !Array.isArray(x);
7
+ const text = (x) => typeof x === "string" && x.length > 0;
8
+ const strings = (x) =>
9
+ Array.isArray(x) && x.every((v) => typeof v === "string");
10
+ const hash = (x) => typeof x === "string" && /^[a-f0-9]{64}$/.test(x);
11
+ const integer = (x) => Number.isSafeInteger(x) && x >= 0;
12
+ const severityRank = { degraded: 0, "feature-dead": 1, fatal: 2 };
13
+ const certaintyRank = { suspected: 0, certain: 1 };
14
+ const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
15
+ const findingKey = (plugin, f) =>
16
+ JSON.stringify([
17
+ plugin,
18
+ f.category,
19
+ f.service ?? null,
20
+ f.method ?? null,
21
+ f.location?.file ?? null,
22
+ f.location?.line ?? null,
23
+ f.location?.column ?? null,
24
+ f.location?.start ?? null,
25
+ f.location?.end ?? null,
26
+ ]);
27
+
28
+ /** Keys retain plugin and exact source span; severity and certainty change independently. */
29
+ export function compareUpgradeFindings(currentReport, targetReport) {
30
+ const changes = {
31
+ added: [],
32
+ worsened: [],
33
+ resolved: [],
34
+ unchanged: [],
35
+ improved: [],
36
+ };
37
+ const collect = (report) => {
38
+ const entries = new Map();
39
+ for (const plugin of Object.keys(report.plugins).sort())
40
+ for (const finding of report.plugins[plugin].findings) {
41
+ const key = findingKey(plugin, finding);
42
+ if (!entries.has(key)) entries.set(key, []);
43
+ entries.get(key).push({ plugin, finding });
44
+ }
45
+ return entries;
46
+ };
47
+ const before = collect(currentReport),
48
+ after = collect(targetReport);
49
+ for (const key of [...new Set([...before.keys(), ...after.keys()])].sort()) {
50
+ const old = before.get(key) || [],
51
+ next = after.get(key) || [];
52
+ for (let i = 0; i < Math.max(old.length, next.length); i++) {
53
+ if (!old[i]) {
54
+ changes.added.push(next[i]);
55
+ continue;
56
+ }
57
+ if (!next[i]) {
58
+ changes.resolved.push(old[i]);
59
+ continue;
60
+ }
61
+ const a = old[i].finding,
62
+ b = next[i].finding;
63
+ const severity = severityRank[b.severity] - severityRank[a.severity];
64
+ const certainty = certaintyRank[b.certainty] - certaintyRank[a.certainty];
65
+ if (severity > 0 || certainty > 0)
66
+ changes.worsened.push({ plugin: old[i].plugin, before: a, after: b });
67
+ else if (severity < 0 || certainty < 0)
68
+ changes.improved.push({ plugin: old[i].plugin, before: a, after: b });
69
+ else changes.unchanged.push(next[i]);
70
+ }
71
+ }
72
+ return changes;
73
+ }
74
+ export function upgradeExitCode(changes) {
75
+ const risks = [
76
+ ...changes.added.map((x) => x.finding),
77
+ ...changes.worsened.map((x) => x.after),
78
+ ];
79
+ return risks.some((f) => f.severity === "fatal") ? 2 : risks.length ? 1 : 0;
80
+ }
81
+ function finding(f) {
82
+ return (
83
+ record(f) &&
84
+ text(f.category) &&
85
+ Object.hasOwn(severityRank, f.severity) &&
86
+ Object.hasOwn(certaintyRank, f.certainty) &&
87
+ ["inferred", "measured"].includes(f.impactBasis) &&
88
+ text(f.impact) &&
89
+ text(f.basis) &&
90
+ (f.evidence === undefined || record(f.evidence)) &&
91
+ [f.service, f.method].every(
92
+ (x) => x === undefined || x === null || typeof x === "string",
93
+ ) &&
94
+ (f.location === null ||
95
+ (record(f.location) &&
96
+ text(f.location.file) &&
97
+ Number.isSafeInteger(f.location.line) &&
98
+ f.location.line > 0 &&
99
+ ["column", "start", "end"].every(
100
+ (k) =>
101
+ f.location[k] === undefined ||
102
+ f.location[k] === null ||
103
+ integer(f.location[k]),
104
+ ) &&
105
+ (!(integer(f.location.start) && integer(f.location.end)) ||
106
+ f.location.end >= f.location.start)))
107
+ );
108
+ }
109
+ function report(body, version, artifactHash) {
110
+ if (
111
+ !record(body) ||
112
+ body.schemaVersion !== 1 ||
113
+ body.kind !== "plugin-compat-check/result" ||
114
+ body.ok !== true ||
115
+ body.error !== null ||
116
+ body.dshVersion !== version ||
117
+ !text(body.toolVersion) ||
118
+ body.baseline !== null ||
119
+ body.cache !== null ||
120
+ !record(body.inputs) ||
121
+ body.inputs.artifactSha256 !== artifactHash ||
122
+ body.inputs.hasRuntimeSurface !== false ||
123
+ !record(body.observation) ||
124
+ body.observation.mode !== "catalogue" ||
125
+ body.observation.usable !== false ||
126
+ body.observation.bound !== false ||
127
+ body.observation.profileId !== null ||
128
+ body.observation.dshVersion !== null ||
129
+ body.observation.probedAt !== null ||
130
+ !strings(body.observation.notes) ||
131
+ !record(body.plugins) ||
132
+ !record(body.summary) ||
133
+ !record(body.extraction) ||
134
+ typeof body.extraction.scanComplete !== "boolean" ||
135
+ !Array.isArray(body.extraction.diagnostics) ||
136
+ !Array.isArray(body.extraction.manifests)
137
+ )
138
+ return false;
139
+ const counts = {
140
+ ok: 0,
141
+ partial: 0,
142
+ broken: 0,
143
+ unknown: 0,
144
+ limited: 0,
145
+ vacuousOk: 0,
146
+ };
147
+ for (const [name, p] of Object.entries(body.plugins)) {
148
+ if (
149
+ !record(p) ||
150
+ p.name !== name ||
151
+ !(p.version === null || typeof p.version === "string") ||
152
+ !Object.hasOwn(counts, p.verdict) ||
153
+ ["limited", "vacuousOk"].includes(p.verdict) ||
154
+ typeof p.vacuous !== "boolean" ||
155
+ !Array.isArray(p.findings) ||
156
+ !p.findings.every(finding) ||
157
+ !record(p.coverage) ||
158
+ !["complete", "limited"].includes(p.coverage.status) ||
159
+ !integer(p.coverage.verifiedCalls) ||
160
+ !strings(p.coverage.limitations) ||
161
+ !record(p.resolved)
162
+ )
163
+ return false;
164
+ for (const key of [
165
+ "checked",
166
+ "clientSide",
167
+ "filtered",
168
+ "abstained",
169
+ "runtimeFacilities",
170
+ ])
171
+ if (!Array.isArray(p[key])) return false;
172
+ if (
173
+ p.verdict !== "unknown" &&
174
+ p.verdict !==
175
+ (p.findings.some((f) => f.severity === "fatal")
176
+ ? "broken"
177
+ : p.findings.length
178
+ ? "partial"
179
+ : "ok")
180
+ )
181
+ return false;
182
+ if (
183
+ p.vacuous !== (p.verdict === "ok" && p.coverage.verifiedCalls === 0) ||
184
+ (p.coverage.status === "limited") !==
185
+ (p.coverage.limitations.length > 0 ||
186
+ p.clientSide.length > 0 ||
187
+ p.vacuous)
188
+ )
189
+ return false;
190
+ const dispositions = [
191
+ "finding",
192
+ "client-side",
193
+ "self-provided",
194
+ "runtime-facility",
195
+ "filtered",
196
+ "abstained",
197
+ "guarded",
198
+ "no-calls",
199
+ "unknown",
200
+ ];
201
+ const checked = (x) =>
202
+ record(x) && text(x.service) && dispositions.includes(x.disposition);
203
+ if (
204
+ !p.checked.every(checked) ||
205
+ !Object.entries(p.resolved).every(
206
+ ([svc, x]) => checked(x) && x.service === svc,
207
+ ) ||
208
+ !p.clientSide.every(
209
+ (x) => record(x) && text(x.service) && text(x.note),
210
+ ) ||
211
+ !p.filtered.every(
212
+ (x) => record(x) && text(x.service) && text(x.method) && text(x.reason),
213
+ ) ||
214
+ !p.abstained.every(
215
+ (x) => record(x) && text(x.service) && text(x.basis),
216
+ ) ||
217
+ !p.runtimeFacilities.every(
218
+ (x) => record(x) && text(x.name) && text(x.reason),
219
+ )
220
+ )
221
+ return false;
222
+ counts[p.verdict]++;
223
+ if (p.coverage.status === "limited") counts.limited++;
224
+ if (p.vacuous && p.verdict === "ok") counts.vacuousOk++;
225
+ }
226
+ return (
227
+ Object.entries(counts).every(([k, v]) => body.summary[k] === v) &&
228
+ body.exitCode ===
229
+ (counts.broken ? 2 : counts.partial || counts.unknown ? 1 : 0)
230
+ );
231
+ }
232
+ export function assertUpgradePreview(value) {
233
+ const errors = [],
234
+ need = (ok, key) => {
235
+ if (!ok) errors.push(key);
236
+ };
237
+ if (!record(value)) throw new Error("升级预检契约不匹配: preview");
238
+ need(
239
+ value.schemaVersion === 1 &&
240
+ value.kind === "plugin-compat-check/upgrade-preview",
241
+ "schemaVersion/kind",
242
+ );
243
+ need(
244
+ value.mode === "static-version-comparison" &&
245
+ ["compared", "unsupported-version", "error"].includes(value.status),
246
+ "mode/status",
247
+ );
248
+ need(
249
+ text(value.toolVersion) &&
250
+ value.baseline === null &&
251
+ value.cache === null &&
252
+ strings(value.limitations),
253
+ "identity/isolation",
254
+ );
255
+ need(
256
+ [value.currentVersion, value.targetVersion].every(
257
+ (x) => x === null || typeof x === "string",
258
+ ),
259
+ "versions",
260
+ );
261
+ need(
262
+ same(value.supportedVersions, SUPPORTED_PREVIEW_VERSIONS),
263
+ "supportedVersions",
264
+ );
265
+ const groups = ["added", "worsened", "resolved", "unchanged", "improved"];
266
+ need(record(value.changes) && record(value.summary), "changes/summary");
267
+ for (const key of groups) {
268
+ need(Array.isArray(value.changes?.[key]), "changes." + key);
269
+ need(
270
+ integer(value.summary?.[key]) &&
271
+ value.summary[key] === value.changes?.[key]?.length,
272
+ "summary." + key,
273
+ );
274
+ for (const item of Array.isArray(value.changes?.[key])
275
+ ? value.changes[key]
276
+ : [])
277
+ need(
278
+ record(item) &&
279
+ text(item.plugin) &&
280
+ (["worsened", "improved"].includes(key)
281
+ ? finding(item.before) && finding(item.after)
282
+ : finding(item.finding)),
283
+ "finding." + key,
284
+ );
285
+ }
286
+ need(integer(value.summary?.limitedPlugins), "summary.limitedPlugins");
287
+ if (value.status === "compared") {
288
+ need(
289
+ value.ok === true &&
290
+ value.error === null &&
291
+ SUPPORTED_PREVIEW_VERSIONS.includes(value.currentVersion) &&
292
+ SUPPORTED_PREVIEW_VERSIONS.includes(value.targetVersion),
293
+ "compared.versions/ok",
294
+ );
295
+ need(record(value.catalogs), "catalogs");
296
+ for (const side of ["current", "target"]) {
297
+ const item = value.catalogs?.[side];
298
+ need(
299
+ record(item) &&
300
+ item.version === value[side + "Version"] &&
301
+ text(item.file) &&
302
+ hash(item.sha256) &&
303
+ record(item.source) &&
304
+ text(item.source.host) &&
305
+ text(item.source.client),
306
+ "catalog." + side,
307
+ );
308
+ }
309
+ need(
310
+ record(value.rules) && text(value.rules.file) && hash(value.rules.sha256),
311
+ "rules",
312
+ );
313
+ need(
314
+ record(value.artifact) &&
315
+ hash(value.artifact.sha256) &&
316
+ Array.isArray(value.artifact.packages) &&
317
+ value.artifact.packages.every(
318
+ (p) =>
319
+ record(p) &&
320
+ text(p.name) &&
321
+ (p.version === null || typeof p.version === "string") &&
322
+ hash(p.sha256),
323
+ ),
324
+ "artifact",
325
+ );
326
+ const validReports =
327
+ report(
328
+ value.currentReport,
329
+ value.currentVersion,
330
+ value.artifact?.sha256,
331
+ ) &&
332
+ report(value.targetReport, value.targetVersion, value.artifact?.sha256);
333
+ need(validReports, "canonicalReports");
334
+ if (validReports) {
335
+ const identities = (body) =>
336
+ Object.values(body.plugins)
337
+ .map((p) => JSON.stringify([p.name, p.version]))
338
+ .sort();
339
+ const currentIdentities = identities(value.currentReport);
340
+ const targetIdentities = identities(value.targetReport);
341
+ const artifactIdentities = Array.isArray(value.artifact?.packages)
342
+ ? value.artifact.packages.map((p) =>
343
+ JSON.stringify([p?.name, p?.version]),
344
+ )
345
+ : [];
346
+ const artifactIdentitySet = new Set(artifactIdentities),
347
+ currentIdentitySet = new Set(currentIdentities);
348
+ need(
349
+ same(currentIdentities, targetIdentities),
350
+ "canonicalReports.same-input",
351
+ );
352
+ need(
353
+ artifactIdentitySet.size === artifactIdentities.length &&
354
+ currentIdentities.length === artifactIdentities.length &&
355
+ currentIdentities.every((identity) =>
356
+ artifactIdentitySet.has(identity),
357
+ ) &&
358
+ artifactIdentities.every((identity) =>
359
+ currentIdentitySet.has(identity),
360
+ ),
361
+ "artifact.package-identities",
362
+ );
363
+ need(
364
+ same(
365
+ value.changes,
366
+ compareUpgradeFindings(value.currentReport, value.targetReport),
367
+ ),
368
+ "changes.evidence",
369
+ );
370
+ const limited = new Set(
371
+ [
372
+ ...Object.values(value.currentReport.plugins),
373
+ ...Object.values(value.targetReport.plugins),
374
+ ]
375
+ .filter((p) => p.coverage.status === "limited")
376
+ .map((p) => p.name),
377
+ );
378
+ need(
379
+ value.summary?.limitedPlugins === limited.size,
380
+ "summary.limitedPlugins/count",
381
+ );
382
+ }
383
+ if (!errors.length)
384
+ need(value.exitCode === upgradeExitCode(value.changes), "exitCode");
385
+ } else {
386
+ need(
387
+ value.ok === false && text(value.error) && value.exitCode === 3,
388
+ "failure.error/exitCode",
389
+ );
390
+ need(
391
+ ["artifact", "catalogs", "rules", "currentReport", "targetReport"].every(
392
+ (k) => value[k] === null,
393
+ ),
394
+ "failure.evidence",
395
+ );
396
+ need(
397
+ groups.every((k) => value.changes?.[k]?.length === 0) &&
398
+ value.summary?.limitedPlugins === 0,
399
+ "failure.changes",
400
+ );
401
+ if (value.status === "unsupported-version")
402
+ need(
403
+ !SUPPORTED_PREVIEW_VERSIONS.includes(value.currentVersion) ||
404
+ !SUPPORTED_PREVIEW_VERSIONS.includes(value.targetVersion),
405
+ "unsupported-version",
406
+ );
407
+ }
408
+ if (errors.length)
409
+ throw new Error("升级预检契约不匹配: " + errors.join(", "));
410
+ return value;
411
+ }
412
+
413
+ export const UPGRADE_PREVIEW_LIMITATIONS = Object.freeze([
414
+ "静态版本预测未认证未来环境或业务通过;没有新增风险仅表示本次未发现新增静态风险。",
415
+ "对同一组插件的私有字节副本联合静态提取一次,识别名集合取两个目录的并集;随后各按对应版本目录独立判定,不能复用普通实时体检证明未来环境。",
416
+ "API目录不是运行时服务面;确定性、推断影响、覆盖限制、无已验证调用及客户端待验证信息均保留。",
417
+ "当前宿主运行时、探针、条目状态、profile豁免、缓存和基线不参与预测;不执行插件代码、不安装或升级。",
418
+ "输入采用有界复制及两次一致性观察,检测普通并发变更;不是操作系统原子快照。",
419
+ "退出码仅反映新增或变重风险:0无新增,1新增非致命,2新增致命,3预检不可用;目标版本原有风险仍列出。",
420
+ ]);
421
+ const emptyChanges = () => ({
422
+ added: [],
423
+ worsened: [],
424
+ resolved: [],
425
+ unchanged: [],
426
+ improved: [],
427
+ });
428
+ export function createUpgradeFailure(error, options = {}) {
429
+ if (error && typeof error === "object" && !(error instanceof Error)) {
430
+ options = error;
431
+ error = options.error;
432
+ }
433
+ const value = {
434
+ schemaVersion: 1,
435
+ kind: "plugin-compat-check/upgrade-preview",
436
+ toolVersion: text(options.toolVersion) ? options.toolVersion : "unknown",
437
+ mode: "static-version-comparison",
438
+ ok: false,
439
+ error: String(error?.message || error || "升级预检不可用"),
440
+ exitCode: 3,
441
+ status:
442
+ options.status === "unsupported-version"
443
+ ? "unsupported-version"
444
+ : "error",
445
+ currentVersion:
446
+ typeof options.currentVersion === "string"
447
+ ? options.currentVersion
448
+ : null,
449
+ targetVersion:
450
+ typeof options.targetVersion === "string" ? options.targetVersion : null,
451
+ supportedVersions: [...SUPPORTED_PREVIEW_VERSIONS],
452
+ catalogs: null,
453
+ artifact: null,
454
+ rules: null,
455
+ currentReport: null,
456
+ targetReport: null,
457
+ changes: emptyChanges(),
458
+ summary: {
459
+ added: 0,
460
+ worsened: 0,
461
+ resolved: 0,
462
+ unchanged: 0,
463
+ improved: 0,
464
+ limitedPlugins: 0,
465
+ },
466
+ limitations: [...UPGRADE_PREVIEW_LIMITATIONS],
467
+ baseline: null,
468
+ cache: null,
469
+ };
470
+ return assertUpgradePreview(value);
471
+ }