dsh-plugin-compat-check 0.1.4 → 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 (43) hide show
  1. package/README.md +37 -17
  2. package/THIRD_PARTY_NOTICES.md +139 -0
  3. package/index.js +282 -53
  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/report-identity-worker.mjs +153 -0
  17. package/kernel/lib/report-identity.mjs +15 -0
  18. package/kernel/lib/report-service.mjs +196 -11
  19. package/kernel/lib/report.mjs +76 -11
  20. package/kernel/lib/source-analysis.mjs +24 -6
  21. package/kernel/lib/upgrade-capture-worker.mjs +20 -0
  22. package/kernel/lib/upgrade-cleanup-worker.mjs +28 -0
  23. package/kernel/lib/upgrade-contract.mjs +471 -0
  24. package/kernel/lib/upgrade-input.mjs +309 -0
  25. package/kernel/lib/upgrade-preview.mjs +256 -0
  26. package/kernel/lib/upgrade-worker.mjs +50 -0
  27. package/kernel/lib/version.mjs +1 -1
  28. package/kernel/lib/worker-analysis.mjs +16 -1
  29. package/kernel/lib/workflow-panels.mjs +706 -0
  30. package/kernel/rules/README.md +5 -5
  31. package/lib/client.js +2974 -69
  32. package/lib/dependency-graph.js +651 -0
  33. package/lib/finding-guidance.js +73 -0
  34. package/lib/finding-review.js +339 -0
  35. package/lib/functional-health-contract.js +338 -0
  36. package/lib/report-contract.js +88 -0
  37. package/lib/upgrade-contract.js +472 -0
  38. package/lib/workflow-panels.js +707 -0
  39. package/package.json +3 -2
  40. package/scripts/prepublish-check.mjs +28 -13
  41. package/scripts/stage-contract.mjs +91 -29
  42. package/scripts/stage-kernel.mjs +3 -2
  43. package/skill/SKILL.md +90 -39
@@ -0,0 +1,338 @@
1
+ // Generated from lib/functional-health-contract.mjs; run node plugin/scripts/stage-contract.mjs
2
+ /** Pure declarations/results contract. Author declarations never imply execution or compatibility. */
3
+ export const FUNCTIONAL_HEALTH_KIND = "plugin-compat-check/functional-health";
4
+ export const FUNCTIONAL_HEALTH_LIMITS = Object.freeze({
5
+ checks: 32,
6
+ path: 256,
7
+ selector: 160,
8
+ responseBytes: 65536,
9
+ requestMs: 3000,
10
+ totalMs: 180000,
11
+ });
12
+ const object = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
13
+ const keys = (v, allowed) =>
14
+ object(v) && Object.keys(v).every((k) => allowed.includes(k));
15
+ const exactVersion = (v) =>
16
+ typeof v === "string" &&
17
+ /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[0-9A-Za-z]+(?:[.-][0-9A-Za-z]+)*)?$/.test(
18
+ v,
19
+ );
20
+ const digest = (v) => typeof v === "string" && /^[a-f0-9]{64}$/.test(v);
21
+ const bounded = (v, n) =>
22
+ typeof v === "string" && v.length > 0 && v.length <= n;
23
+ const artifactValid = (a) =>
24
+ keys(a, ["name", "version", "sha256"]) &&
25
+ typeof a.name === "string" &&
26
+ /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/.test(a.name) &&
27
+ a.name.length <= 214 &&
28
+ exactVersion(a.version) &&
29
+ digest(a.sha256);
30
+ export function validFunctionalPath(v) {
31
+ return (
32
+ bounded(v, FUNCTIONAL_HEALTH_LIMITS.path) &&
33
+ /^\/(?!\/)[A-Za-z0-9/_.,~-]*$/.test(v) &&
34
+ !v.split("/").some((p) => p === ".." || p === ".")
35
+ );
36
+ }
37
+ export function validateFunctionalManifest(value) {
38
+ const errors = [];
39
+ if (
40
+ !keys(value, ["schemaVersion", "artifact", "dshVersion", "checks"]) ||
41
+ value.schemaVersion !== 1 ||
42
+ !artifactValid(value.artifact) ||
43
+ !exactVersion(value.dshVersion)
44
+ )
45
+ errors.push("Invalid manifest schema or exact artifact/DSH identity");
46
+ if (
47
+ !Array.isArray(value?.checks) ||
48
+ value.checks.length < 1 ||
49
+ value.checks.length > FUNCTIONAL_HEALTH_LIMITS.checks
50
+ )
51
+ errors.push("Expected 1..32 bounded checks");
52
+ else {
53
+ const seen = new Set();
54
+ for (const c of value.checks) {
55
+ if (
56
+ !object(c) ||
57
+ !bounded(c.id, 64) ||
58
+ !/^[A-Za-z0-9_-]+$/.test(c.id) ||
59
+ seen.has(c.id) ||
60
+ !validFunctionalPath(c.path)
61
+ ) {
62
+ errors.push("Invalid check id/path or duplicate id");
63
+ continue;
64
+ }
65
+ seen.add(c.id);
66
+ if (c.type === "http") {
67
+ if (
68
+ !keys(c, ["id", "type", "path", "status", "json"]) ||
69
+ !Number.isInteger(c.status) ||
70
+ c.status < 200 ||
71
+ c.status > 599 ||
72
+ (c.status >= 300 && c.status < 400)
73
+ )
74
+ errors.push("Invalid HTTP check");
75
+ if (
76
+ c.json !== undefined &&
77
+ (!keys(c.json, ["pointer", "equals"]) ||
78
+ typeof c.json.pointer !== "string" ||
79
+ c.json.pointer.length > 256 ||
80
+ !/^(?:\/(?:[^~\u0000-\u001f]|~[01])*)*$/.test(c.json.pointer) ||
81
+ !Object.hasOwn(c.json, "equals") ||
82
+ ![null, "string", "boolean", "number"].includes(
83
+ c.json.equals === null ? null : typeof c.json.equals,
84
+ ) ||
85
+ (typeof c.json.equals === "string" && c.json.equals.length > 256) ||
86
+ (typeof c.json.equals === "number" &&
87
+ !Number.isFinite(c.json.equals)))
88
+ )
89
+ errors.push(
90
+ "JSON assertion must be a bounded JSON pointer and scalar value",
91
+ );
92
+ } else if (c.type === "dom") {
93
+ if (
94
+ !keys(c, [
95
+ "id",
96
+ "type",
97
+ "path",
98
+ "selector",
99
+ "minCount",
100
+ "maxCount",
101
+ ]) ||
102
+ !bounded(c.selector, FUNCTIONAL_HEALTH_LIMITS.selector) ||
103
+ !/^[A-Za-z0-9_#.*\[\]="'\- >]+$/.test(c.selector) ||
104
+ !Number.isInteger(c.minCount) ||
105
+ !Number.isInteger(c.maxCount) ||
106
+ c.minCount < 0 ||
107
+ c.maxCount < c.minCount ||
108
+ c.maxCount > 1000
109
+ )
110
+ errors.push("Invalid bounded DOM selector/count check");
111
+ } else errors.push("Only HTTP GET and DOM selector counts are supported");
112
+ }
113
+ }
114
+ return errors.length ? { valid: false, errors } : { valid: true, value };
115
+ }
116
+
117
+ /** Imported evidence is an offline assertion, not a signature or compatibility verdict. */
118
+ export function validateFunctionalHealthResult(value, expectedIdentity = {}) {
119
+ const errors = [];
120
+ const iso = (v) =>
121
+ typeof v === "string" &&
122
+ Number.isFinite(Date.parse(v)) &&
123
+ new Date(v).toISOString() === v;
124
+ if (
125
+ !keys(value, [
126
+ "schemaVersion",
127
+ "kind",
128
+ "status",
129
+ "identity",
130
+ "startedAt",
131
+ "completedAt",
132
+ "checks",
133
+ "cleanup",
134
+ "limitations",
135
+ "error",
136
+ "declaration",
137
+ ]) ||
138
+ value.schemaVersion !== 1 ||
139
+ value.kind !== FUNCTIONAL_HEALTH_KIND ||
140
+ !["tested-pass", "tested-failed", "error"].includes(value.status)
141
+ )
142
+ errors.push("Invalid functional health result schema/status");
143
+ const declarationValidation = validateFunctionalManifest(value?.declaration);
144
+ if (!declarationValidation.valid)
145
+ errors.push("Missing or invalid executed declaration");
146
+ const id = value?.identity;
147
+ if (
148
+ declarationValidation.valid &&
149
+ (value.declaration.dshVersion !== id?.dshVersion ||
150
+ ["name", "version", "sha256"].some(
151
+ (k) => value.declaration.artifact[k] !== id?.artifact?.[k],
152
+ ))
153
+ )
154
+ errors.push("Executed declaration identity differs from result");
155
+ if (
156
+ !keys(id, ["artifact", "dshVersion", "manifestSha256"]) ||
157
+ !artifactValid(id.artifact) ||
158
+ !exactVersion(id.dshVersion) ||
159
+ !digest(id.manifestSha256)
160
+ )
161
+ errors.push("Invalid exact evidence identity");
162
+ if (
163
+ !iso(value?.startedAt) ||
164
+ !iso(value?.completedAt) ||
165
+ value.completedAt < value.startedAt ||
166
+ Date.parse(value.completedAt) - Date.parse(value.startedAt) >
167
+ FUNCTIONAL_HEALTH_LIMITS.totalMs + 30000
168
+ )
169
+ errors.push("Invalid bounded measurement times");
170
+ if (
171
+ !keys(value?.cleanup, ["stopped", "removed", "errors"]) ||
172
+ typeof value.cleanup.stopped !== "boolean" ||
173
+ typeof value.cleanup.removed !== "boolean" ||
174
+ (value.cleanup.errors !== undefined &&
175
+ (!Array.isArray(value.cleanup.errors) ||
176
+ value.cleanup.errors.length > 8 ||
177
+ value.cleanup.errors.some((x) => !bounded(x, 512))))
178
+ )
179
+ errors.push("Invalid cleanup evidence");
180
+ if (
181
+ !Array.isArray(value?.limitations) ||
182
+ value.limitations.length < 1 ||
183
+ value.limitations.length > 8 ||
184
+ value.limitations.some((x) => !bounded(x, 512)) ||
185
+ (value.error !== undefined && !bounded(value.error, 1024))
186
+ )
187
+ errors.push("Invalid limitations/error summary");
188
+ const seen = new Set();
189
+ if (!Array.isArray(value?.checks) || value.checks.length > 32)
190
+ errors.push("Invalid check evidence");
191
+ else
192
+ for (const c of value.checks) {
193
+ const e = c?.evidence;
194
+ if (
195
+ !keys(c, ["id", "type", "status", "evidence"]) ||
196
+ !bounded(c.id, 64) ||
197
+ !/^[A-Za-z0-9_-]+$/.test(c.id) ||
198
+ seen.has(c.id) ||
199
+ !["http", "dom"].includes(c.type) ||
200
+ !["pass", "failed", "error"].includes(c.status) ||
201
+ !keys(e, [
202
+ "httpStatus",
203
+ "responseBytes",
204
+ "jsonMatched",
205
+ "count",
206
+ "elapsedMs",
207
+ "error",
208
+ ]) ||
209
+ !Number.isFinite(e.elapsedMs) ||
210
+ e.elapsedMs < 0 ||
211
+ e.elapsedMs > FUNCTIONAL_HEALTH_LIMITS.totalMs
212
+ ) {
213
+ errors.push("Invalid check row");
214
+ continue;
215
+ }
216
+ seen.add(c.id);
217
+ if (c.status === "error") {
218
+ if (!bounded(e.error, 256)) errors.push("Missing check error");
219
+ } else if (c.type === "http") {
220
+ if (
221
+ !Number.isInteger(e.httpStatus) ||
222
+ e.httpStatus < 200 ||
223
+ e.httpStatus > 599 ||
224
+ !Number.isInteger(e.responseBytes) ||
225
+ e.responseBytes < 0 ||
226
+ e.responseBytes > 65536 ||
227
+ (e.jsonMatched !== undefined && typeof e.jsonMatched !== "boolean") ||
228
+ e.count !== undefined ||
229
+ e.error !== undefined
230
+ )
231
+ errors.push("Invalid HTTP evidence");
232
+ } else if (
233
+ !Number.isInteger(e.count) ||
234
+ e.count < 0 ||
235
+ e.httpStatus !== undefined ||
236
+ e.responseBytes !== undefined ||
237
+ e.jsonMatched !== undefined ||
238
+ e.error !== undefined
239
+ )
240
+ errors.push("Invalid DOM evidence");
241
+ }
242
+ if (errors.length) return { valid: false, errors };
243
+ if (declarationValidation.valid && Array.isArray(value?.checks)) {
244
+ const expected = value.declaration.checks;
245
+ if (
246
+ (value.status !== "error" && value.checks.length !== expected.length) ||
247
+ value.checks.length > expected.length
248
+ )
249
+ errors.push("Evidence must cover every declared check in order");
250
+ for (let i = 0; i < value.checks.length; i++) {
251
+ const row = value.checks[i],
252
+ check = expected[i];
253
+ if (!row || !check || row.id !== check.id || row.type !== check.type) {
254
+ errors.push("Evidence check identity/order differs from declaration");
255
+ continue;
256
+ }
257
+ if (row.status === "error") continue;
258
+ const e = row.evidence;
259
+ if (!object(e)) continue;
260
+ const passed =
261
+ check.type === "http"
262
+ ? e.httpStatus === check.status &&
263
+ (!check.json || e.jsonMatched === true)
264
+ : e.count >= check.minCount && e.count <= check.maxCount;
265
+ if (
266
+ row.status !== (passed ? "pass" : "failed") ||
267
+ (check.type === "http" &&
268
+ Boolean(check.json) !== (e.jsonMatched !== undefined))
269
+ )
270
+ errors.push("Evidence outcome conflicts with executed expectation");
271
+ }
272
+ }
273
+ if (
274
+ value?.status === "tested-pass" &&
275
+ (!value.checks?.length ||
276
+ value.checks.some((c) => c?.status !== "pass") ||
277
+ value.cleanup?.stopped !== true ||
278
+ value.cleanup?.removed !== true ||
279
+ value.cleanup?.errors?.length ||
280
+ value.error !== undefined)
281
+ )
282
+ errors.push("Pass conflicts with checks or cleanup");
283
+ if (
284
+ value?.status === "tested-failed" &&
285
+ (!value.checks?.some((c) => c?.status === "failed") ||
286
+ value.checks.some((c) => c?.status === "error") ||
287
+ value.cleanup?.stopped !== true ||
288
+ value.cleanup?.removed !== true ||
289
+ value.error !== undefined)
290
+ )
291
+ errors.push("Failed measurement conflicts with checks/cleanup");
292
+ if (value?.status === "error" && !bounded(value.error, 1024))
293
+ errors.push("Missing run error");
294
+ if (errors.length) return { valid: false, errors };
295
+ const staleReasons = [];
296
+ if (
297
+ !expectedIdentity.artifact ||
298
+ !["name", "version", "sha256"].every(
299
+ (k) => typeof expectedIdentity.artifact[k] === "string",
300
+ )
301
+ )
302
+ staleReasons.push("Current artifact identity unavailable");
303
+ else
304
+ for (const key of ["name", "version", "sha256"])
305
+ if (id.artifact[key] !== expectedIdentity.artifact[key])
306
+ staleReasons.push("Artifact " + key + " changed");
307
+ if (!expectedIdentity.dshVersion)
308
+ staleReasons.push("Current DSH version unavailable");
309
+ else if (id.dshVersion !== expectedIdentity.dshVersion)
310
+ staleReasons.push("DSH version changed");
311
+ if (!expectedIdentity.manifestSha256)
312
+ staleReasons.push("Current declaration identity unavailable");
313
+ else if (id.manifestSha256 !== expectedIdentity.manifestSha256)
314
+ staleReasons.push("Declaration changed");
315
+ return {
316
+ valid: true,
317
+ value,
318
+ current: staleReasons.length === 0,
319
+ staleReasons,
320
+ };
321
+ }
322
+
323
+ /** SHA256 identity rule: UTF-8 of JSON with recursively sorted object keys and preserved array order, no whitespace. */
324
+ export function canonicalFunctionalManifest(value) {
325
+ const validation = validateFunctionalManifest(value);
326
+ if (!validation.valid) throw Error(validation.errors.join("; "));
327
+ const ordered = (v) =>
328
+ Array.isArray(v)
329
+ ? v.map(ordered)
330
+ : object(v)
331
+ ? Object.fromEntries(
332
+ Object.keys(v)
333
+ .sort()
334
+ .map((k) => [k, ordered(v[k])]),
335
+ )
336
+ : v;
337
+ return JSON.stringify(ordered(value));
338
+ }
@@ -9,6 +9,61 @@
9
9
  export const REPORT_SCHEMA = 1;
10
10
  export const REPORT_KIND = "plugin-compat-check/result";
11
11
  export const ENVELOPE_KIND = "plugin-compat-check/report";
12
+ /** Reviewed catalogue bytes, not a promise of plugin business compatibility.
13
+ * Adding a version requires source verification, rule review and integration
14
+ * evidence; extraction alone must never extend this list. */
15
+ export const CATALOG_REFERENCES = Object.freeze([
16
+ Object.freeze({
17
+ version: "0.2.0-rc.2",
18
+ file: "dsh-api-catalog.json",
19
+ sha256: "1331bf3220c1e06cfe087237077118d002dc04c32d027ce01da74c6a0d1dc9ad",
20
+ }),
21
+ Object.freeze({
22
+ version: "0.2.1-rc.2",
23
+ file: "dsh-api-catalog-0.2.1-rc.2.json",
24
+ sha256: "90b4ae484ebc6660c53e5c9ad14ba657581534b30e8222c636c759e19587f743",
25
+ }),
26
+ ]);
27
+ export function describeCatalog({
28
+ targetVersion = null,
29
+ referenceVersion = null,
30
+ sha256,
31
+ override = false,
32
+ }) {
33
+ const known =
34
+ !override &&
35
+ CATALOG_REFERENCES.find(
36
+ (c) => c.version === referenceVersion && c.sha256 === sha256,
37
+ );
38
+ const verified = Boolean(known && targetVersion === referenceVersion);
39
+ return {
40
+ targetVersion,
41
+ referenceVersion,
42
+ sha256,
43
+ selection: override ? "override" : verified ? "exact" : "fallback",
44
+ verified,
45
+ certifiedVersions: known ? [known.version] : [],
46
+ scope:
47
+ "仅核验内置目录字节及对应精确版本的集成;不代表业务验收或运行时观测。",
48
+ warning: verified
49
+ ? null
50
+ : override
51
+ ? "自定义参考目录未认证;目录自报版本或相同字节不扩大已验收范围,请在实际目标宿主复核。"
52
+ : "目标 DSH 版本未知或未认证;普通体检使用参考目录回退,不能据此认定该版本已验收。",
53
+ };
54
+ }
55
+ export function catalogLines(c) {
56
+ if (!c) return ["API 目录身份未提供;版本范围未认证,请重新体检。"];
57
+ return [
58
+ "目标 DSH:" + (c.targetVersion || "未知"),
59
+ "参考目录:" + (c.referenceVersion || "未知") + " · SHA-256:" + c.sha256,
60
+ "目录认证范围:" +
61
+ (c.certifiedVersions.join("、") || "无") +
62
+ ";" +
63
+ c.scope,
64
+ ...(c.warning ? [c.warning] : []),
65
+ ];
66
+ }
12
67
  const record = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
13
68
  const text = (v) => typeof v === "string" && v.length > 0;
14
69
  const nullableText = (v) => v === null || typeof v === "string";
@@ -283,6 +338,37 @@ export function validateReportBody(body) {
283
338
  }
284
339
  need(e, body.error === null, "report.error");
285
340
  need(e, nullableText(body.dshVersion), "report.dshVersion");
341
+ // Older schema-1 reports remain readable, but consumers show missing identity.
342
+ if (body.catalog !== undefined) {
343
+ const c = body.catalog;
344
+ if (
345
+ !record(c) ||
346
+ !nullableText(c.referenceVersion) ||
347
+ !/^[a-f0-9]{64}$/.test(c.sha256)
348
+ )
349
+ e.push("report.catalog");
350
+ else {
351
+ const expected = describeCatalog({
352
+ targetVersion: body.dshVersion,
353
+ referenceVersion: c.referenceVersion,
354
+ sha256: c.sha256,
355
+ override: c.selection === "override",
356
+ });
357
+ if (
358
+ c.selection !== "override" &&
359
+ !CATALOG_REFERENCES.some(
360
+ (k) => k.version === c.referenceVersion && k.sha256 === c.sha256,
361
+ )
362
+ )
363
+ e.push("report.catalog.identity");
364
+ for (const [key, value] of Object.entries(expected))
365
+ need(
366
+ e,
367
+ JSON.stringify(c[key]) === JSON.stringify(value),
368
+ "report.catalog." + key,
369
+ );
370
+ }
371
+ }
286
372
  e.push(...validateObservation(body.observation));
287
373
  if (
288
374
  record(body.observation) &&
@@ -467,6 +553,8 @@ export function validateEnvelope(data) {
467
553
  )
468
554
  return ["envelope.schemaVersion/kind"];
469
555
  const e = validateReportBody(data.report);
556
+ if (Object.hasOwn(data, "issueReport"))
557
+ need(e, typeof data.issueReport === "string", "envelope.issueReport");
470
558
  need(
471
559
  e,
472
560
  typeof data.ok === "boolean" && data.ok === data.report?.ok,