dsh-plugin-compat-check 0.1.4 → 0.1.6

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 +39 -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 +10 -5
  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,188 @@
1
+ /** Standard CI serializers; outputs describe a static gate, never business acceptance. */
2
+ const relativeLocation = (location) => {
3
+ if (
4
+ !location ||
5
+ typeof location.file !== "string" ||
6
+ !Number.isSafeInteger(location.line) ||
7
+ location.line < 1
8
+ )
9
+ return null;
10
+ const file = location.file.replace(/\\/g, "/");
11
+ if (
12
+ file.startsWith("/") ||
13
+ /^[A-Za-z][A-Za-z\d+.-]*:/.test(file) ||
14
+ file.split("/").some((x) => x === ".." || x === "")
15
+ )
16
+ return null;
17
+ return {
18
+ uri: file.split("/").map(encodeURIComponent).join("/"),
19
+ line: location.line,
20
+ column: location.column,
21
+ };
22
+ };
23
+ const evidence = (m, c, p, f) => ({
24
+ dshVersion: c.dshVersion,
25
+ pluginName: p,
26
+ severity: f.severity,
27
+ certainty: f.certainty,
28
+ impactBasis: f.impactBasis,
29
+ impact: f.impact,
30
+ basis: f.basis,
31
+ identity: c.identity,
32
+ limitations: m.limitations,
33
+ });
34
+ export function renderSarif(matrix) {
35
+ const results = [],
36
+ notifications = [];
37
+ for (const c of matrix.cells || []) {
38
+ if (c.status !== "analyzed") {
39
+ notifications.push({
40
+ level: "error",
41
+ message: { text: c.error || c.status },
42
+ properties: { dshVersion: c.dshVersion, status: c.status },
43
+ });
44
+ continue;
45
+ }
46
+ for (const [name, p] of Object.entries(c.report.plugins))
47
+ for (const f of p.findings) {
48
+ const l = relativeLocation(f.location),
49
+ result = {
50
+ ruleId: f.category,
51
+ level: f.severity === "fatal" ? "error" : "warning",
52
+ message: {
53
+ text: f.impact + " (" + f.certainty + "; " + f.basis + ")",
54
+ },
55
+ properties: evidence(matrix, c, name, f),
56
+ };
57
+ if (l)
58
+ result.locations = [
59
+ {
60
+ physicalLocation: {
61
+ artifactLocation: { uri: l.uri },
62
+ region: {
63
+ startLine: l.line,
64
+ ...(Number.isSafeInteger(l.column) && l.column >= 0
65
+ ? { startColumn: l.column + 1 }
66
+ : {}),
67
+ },
68
+ },
69
+ },
70
+ ];
71
+ results.push(result);
72
+ }
73
+ }
74
+ // Gate-only blocks (coverage, unknown, invalid previous) must survive export too.
75
+ for (const r of matrix.gate.reasons || [])
76
+ if (r.code !== "finding")
77
+ notifications.push({
78
+ level: matrix.gate.exitCode === 3 ? "error" : "warning",
79
+ message: { text: r.message },
80
+ properties: r,
81
+ });
82
+ return {
83
+ $schema: "https://json.schemastore.org/sarif-2.1.0.json",
84
+ version: "2.1.0",
85
+ runs: [
86
+ {
87
+ tool: {
88
+ driver: {
89
+ name: "dsh-plugin-compat-check",
90
+ version: matrix.toolVersion,
91
+ },
92
+ },
93
+ invocations: [
94
+ {
95
+ executionSuccessful: matrix.gate.status !== "error",
96
+ exitCode: matrix.gate.exitCode,
97
+ toolExecutionNotifications: notifications,
98
+ },
99
+ ],
100
+ results,
101
+ properties: {
102
+ identity: matrix.identity,
103
+ policy: matrix.policy,
104
+ gate: matrix.gate,
105
+ limitations: matrix.limitations,
106
+ scope: "Static compatibility evidence; no business acceptance.",
107
+ },
108
+ },
109
+ ],
110
+ };
111
+ }
112
+ const xml = (value) =>
113
+ String(value ?? "")
114
+ .replace(
115
+ /[^\u0009\u000A\u000D\u0020-\uD7FF\uE000-\uFFFD\u{10000}-\u{10FFFF}]/gu,
116
+ "",
117
+ )
118
+ .replace(
119
+ /[&<>"']/g,
120
+ (c) =>
121
+ ({
122
+ "&": "&amp;",
123
+ "<": "&lt;",
124
+ ">": "&gt;",
125
+ '"': "&quot;",
126
+ "'": "&apos;",
127
+ })[c],
128
+ );
129
+ export function renderJUnit(matrix) {
130
+ const cases = [];
131
+ for (const c of matrix.cells || []) {
132
+ const reasons = (matrix.gate.reasons || []).filter(
133
+ (r) => r.dshVersion === c.dshVersion,
134
+ ),
135
+ error =
136
+ c.status !== "analyzed" ||
137
+ (matrix.gate.exitCode === 3 && reasons.length > 0),
138
+ failure = !error && reasons.length > 0;
139
+ const body = {
140
+ dshVersion: c.dshVersion,
141
+ status: c.status,
142
+ error: c.error,
143
+ identity: c.identity,
144
+ kernelExitCode: c.kernelExitCode,
145
+ gateReasons: reasons,
146
+ plugins: c.report?.plugins,
147
+ limitations: matrix.limitations,
148
+ scope: "Static evidence; no business acceptance.",
149
+ };
150
+ const message = c.error || reasons.map((r) => r.message).join("; ");
151
+ cases.push({
152
+ error,
153
+ failure,
154
+ xml: `<testcase classname="author-ci" name="${xml(matrix.identity?.package?.name || "selected-package")} @ ${xml(c.dshVersion)}">${error ? `<error type="ToolError" message="${xml(message)}">${xml(message)}</error>` : failure ? `<failure type="PolicyGate" message="${xml(message)}">${xml(message)}</failure>` : ""}<system-out>${xml(JSON.stringify(body))}</system-out></testcase>`,
155
+ });
156
+ }
157
+ const global = (matrix.gate.reasons || []).filter((r) => !r.dshVersion);
158
+ if (global.length || cases.length === 0) {
159
+ const error = matrix.gate.exitCode === 3,
160
+ failure = !error && matrix.gate.exitCode !== 0,
161
+ message = global.map((r) => r.message).join("; ");
162
+ cases.push({
163
+ error,
164
+ failure,
165
+ xml: `<testcase classname="author-ci" name="matrix gate">${error ? `<error message="${xml(message)}"/>` : failure ? `<failure message="${xml(message)}"/>` : ""}</testcase>`,
166
+ });
167
+ }
168
+ const failures = cases.filter((c) => c.failure).length,
169
+ errors = cases.filter((c) => c.error).length;
170
+ const properties = {
171
+ toolVersion: matrix.toolVersion,
172
+ identity: matrix.identity,
173
+ policy: matrix.policy,
174
+ gate: matrix.gate,
175
+ limitations: matrix.limitations,
176
+ scope: "Static evidence; no business acceptance.",
177
+ };
178
+ return `<?xml version="1.0" encoding="UTF-8"?>\n<testsuites tests="${cases.length}" failures="${failures}" errors="${errors}"><testsuite name="author-ci" tests="${cases.length}" failures="${failures}" errors="${errors}" skipped="0"><properties>${Object.entries(
179
+ properties,
180
+ )
181
+ .map(
182
+ ([name, value]) =>
183
+ `<property name="${xml(name)}" value="${xml(typeof value === "string" ? value : JSON.stringify(value))}"/>`,
184
+ )
185
+ .join(
186
+ "",
187
+ )}</properties>${cases.map((c) => c.xml).join("")}</testsuite></testsuites>\n`;
188
+ }
@@ -0,0 +1,285 @@
1
+ /** Pure author CI gate. Kernel reports remain unchanged and independently useful. */
2
+ import { assertReportBody, CATALOG_REFERENCES } from "./contracts.mjs";
3
+ const severities = ["fatal", "feature-dead", "degraded"];
4
+ const certainties = ["certain", "suspected"];
5
+ export const DEFAULT_POLICY = Object.freeze({
6
+ schemaVersion: 1,
7
+ mode: "all",
8
+ severities: Object.freeze([...severities]),
9
+ certainties: Object.freeze([...certainties]),
10
+ requireCompleteCoverage: true,
11
+ allowVacuous: false,
12
+ });
13
+ const record = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
14
+ const hash = (v) => typeof v === "string" && /^[a-f0-9]{64}$/.test(v);
15
+ const samePackage = (a, b) =>
16
+ a?.name === b?.name && a?.version === b?.version && a?.sha256 === b?.sha256;
17
+ const text = (v) => typeof v === "string" && v.length > 0;
18
+ const need = (condition, message) => {
19
+ if (!condition) throw Error(message);
20
+ };
21
+ export function validatePolicy(policy) {
22
+ need(record(policy), "Policy must be an object");
23
+ need(
24
+ Object.keys(policy).length === Object.keys(DEFAULT_POLICY).length &&
25
+ Object.keys(policy).every((k) => Object.hasOwn(DEFAULT_POLICY, k)),
26
+ "Policy has missing or unknown fields",
27
+ );
28
+ need(
29
+ policy.schemaVersion === 1 && ["all", "new"].includes(policy.mode),
30
+ "Invalid policy schemaVersion or mode",
31
+ );
32
+ for (const [key, allowed] of [
33
+ ["severities", severities],
34
+ ["certainties", certainties],
35
+ ])
36
+ need(
37
+ Array.isArray(policy[key]) &&
38
+ policy[key].every((v) => allowed.includes(v)) &&
39
+ new Set(policy[key]).size === policy[key].length,
40
+ "Invalid policy " + key,
41
+ );
42
+ need(
43
+ typeof policy.requireCompleteCoverage === "boolean" &&
44
+ typeof policy.allowVacuous === "boolean",
45
+ "Invalid policy coverage flags",
46
+ );
47
+ return policy;
48
+ }
49
+ function validIdentity(i) {
50
+ return (
51
+ record(i) &&
52
+ record(i.package) &&
53
+ text(i.package.name) &&
54
+ text(i.package.version) &&
55
+ hash(i.package.sha256) &&
56
+ hash(i.rulesSha256) &&
57
+ hash(i.catalogSha256) &&
58
+ text(i.toolVersion)
59
+ );
60
+ }
61
+ function validateCells(cells, { baseline = false } = {}) {
62
+ need(
63
+ Array.isArray(cells) && cells.length > 0,
64
+ "Matrix requires at least one version cell",
65
+ );
66
+ const seen = new Set();
67
+ let first;
68
+ for (const c of cells) {
69
+ need(
70
+ record(c) && text(c.dshVersion) && !seen.has(c.dshVersion),
71
+ "Missing or duplicate matrix version",
72
+ );
73
+ seen.add(c.dshVersion);
74
+ need(
75
+ ["analyzed", "unsupported", "error"].includes(c.status),
76
+ "Invalid matrix cell status",
77
+ );
78
+ if (c.status !== "analyzed") {
79
+ need(!baseline, "Previous matrix contains unavailable evidence");
80
+ need(c.report === null && text(c.error), "Invalid failed cell");
81
+ continue;
82
+ }
83
+ need(
84
+ c.error === null && validIdentity(c.identity),
85
+ "Invalid analyzed cell identity",
86
+ );
87
+ assertReportBody(c.report);
88
+ need(
89
+ c.kernelExitCode === c.report.exitCode,
90
+ "Cell kernel exit code mismatch",
91
+ );
92
+ const r = c.report,
93
+ i = c.identity,
94
+ cat = CATALOG_REFERENCES.find((x) => x.version === c.dshVersion);
95
+ need(
96
+ r.ok === true &&
97
+ r.dshVersion === c.dshVersion &&
98
+ r.toolVersion === i.toolVersion,
99
+ "Cell report identity mismatch",
100
+ );
101
+ need(
102
+ cat &&
103
+ i.catalogSha256 === cat.sha256 &&
104
+ r.catalog?.verified === true &&
105
+ r.catalog.sha256 === i.catalogSha256,
106
+ "Cell catalogue identity mismatch",
107
+ );
108
+ need(
109
+ Object.keys(r.plugins).length === 1 &&
110
+ r.plugins[i.package.name]?.version === i.package.version,
111
+ "Selected package/report mismatch",
112
+ );
113
+ if (first)
114
+ need(
115
+ samePackage(i.package, first.package) &&
116
+ i.rulesSha256 === first.rulesSha256 &&
117
+ i.toolVersion === first.toolVersion,
118
+ "Cross-cell identity mismatch",
119
+ );
120
+ else first = i;
121
+ }
122
+ }
123
+ function validatePrevious(previous, cells) {
124
+ need(
125
+ record(previous) &&
126
+ previous.schemaVersion === 1 &&
127
+ previous.kind === "plugin-compat-check/author-ci",
128
+ "Previous matrix schema mismatch",
129
+ );
130
+ validatePolicy(previous.policy);
131
+ validateCells(previous.cells, { baseline: true });
132
+ const current = cells.filter((c) => c.status === "analyzed");
133
+ need(
134
+ current.length === cells.length,
135
+ "Current matrix contains unavailable evidence",
136
+ );
137
+ need(
138
+ previous.cells.length === cells.length,
139
+ "Previous version matrix differs",
140
+ );
141
+ need(
142
+ record(previous.identity) && record(previous.identity.package),
143
+ "Previous matrix identity missing",
144
+ );
145
+ const head = previous.cells[0].identity;
146
+ need(
147
+ previous.toolVersion === head.toolVersion &&
148
+ samePackage(previous.identity.package, head.package) &&
149
+ previous.identity.rulesSha256 === head.rulesSha256,
150
+ "Previous matrix identity mismatch",
151
+ );
152
+ for (const c of current) {
153
+ const p = previous.cells.find((x) => x.dshVersion === c.dshVersion);
154
+ need(
155
+ p &&
156
+ p.identity.package.name === c.identity.package.name &&
157
+ p.identity.toolVersion === c.identity.toolVersion &&
158
+ p.identity.rulesSha256 === c.identity.rulesSha256 &&
159
+ p.identity.catalogSha256 === c.identity.catalogSha256,
160
+ "Previous evidence is not comparable",
161
+ );
162
+ }
163
+ }
164
+ function findingKey(f) {
165
+ return JSON.stringify([
166
+ f.category,
167
+ f.service ?? null,
168
+ f.method ?? null,
169
+ f.location?.file ?? null,
170
+ f.location?.line ?? null,
171
+ f.location?.column ?? null,
172
+ f.severity,
173
+ f.certainty,
174
+ ]);
175
+ }
176
+ export function evaluateMatrix(
177
+ cells,
178
+ policy = DEFAULT_POLICY,
179
+ previous = null,
180
+ ) {
181
+ const reasons = [],
182
+ counts = {
183
+ cells: Array.isArray(cells) ? cells.length : 0,
184
+ analyzed: 0,
185
+ findings: 0,
186
+ matched: 0,
187
+ suppressed: 0,
188
+ errors: 0,
189
+ };
190
+ const finish = (exitCode) => ({
191
+ status: exitCode === 0 ? "pass" : exitCode === 3 ? "error" : "blocked",
192
+ exitCode,
193
+ reasons,
194
+ counts,
195
+ });
196
+ try {
197
+ validatePolicy(policy);
198
+ validateCells(cells);
199
+ if (policy.mode === "new") validatePrevious(previous, cells);
200
+ } catch (error) {
201
+ reasons.push({
202
+ dshVersion: null,
203
+ code: "invalid-evidence",
204
+ message: error.message,
205
+ });
206
+ counts.errors++;
207
+ return finish(3);
208
+ }
209
+ let fatal = false,
210
+ risk = false,
211
+ tool = false;
212
+ for (const c of cells) {
213
+ if (c.status !== "analyzed") {
214
+ tool = true;
215
+ counts.errors++;
216
+ reasons.push({
217
+ dshVersion: c.dshVersion,
218
+ code: c.status === "unsupported" ? "unsupported-version" : "cell-error",
219
+ message: c.error,
220
+ });
221
+ continue;
222
+ }
223
+ counts.analyzed++;
224
+ const add = (code, message, pluginName) => {
225
+ risk = true;
226
+ reasons.push({
227
+ dshVersion: c.dshVersion,
228
+ ...(pluginName ? { pluginName } : {}),
229
+ code,
230
+ message,
231
+ });
232
+ };
233
+ if (c.report.extraction.scanComplete !== true)
234
+ add("incomplete-extraction", "Selected source extraction is incomplete");
235
+ for (const [pluginName, p] of Object.entries(c.report.plugins)) {
236
+ if (p.verdict === "unknown")
237
+ add("unknown-verdict", "Plugin verdict is unknown", pluginName);
238
+ if (policy.requireCompleteCoverage && p.coverage.status !== "complete")
239
+ add(
240
+ "incomplete-coverage",
241
+ p.coverage.limitations.join("; ") || "Coverage is limited",
242
+ pluginName,
243
+ );
244
+ if (!policy.allowVacuous && p.vacuous)
245
+ add(
246
+ "vacuous",
247
+ "No verified calls; this is not business acceptance",
248
+ pluginName,
249
+ );
250
+ const prev =
251
+ policy.mode === "new"
252
+ ? previous.cells.find((x) => x.dshVersion === c.dshVersion).report
253
+ .plugins[pluginName]
254
+ : null;
255
+ const old = new Map();
256
+ for (const f of prev?.findings || [])
257
+ old.set(findingKey(f), (old.get(findingKey(f)) || 0) + 1);
258
+ p.findings.forEach((f, findingIndex) => {
259
+ counts.findings++;
260
+ if (
261
+ !policy.severities.includes(f.severity) ||
262
+ !policy.certainties.includes(f.certainty)
263
+ )
264
+ return;
265
+ const key = findingKey(f);
266
+ if (old.get(key) > 0) {
267
+ old.set(key, old.get(key) - 1);
268
+ counts.suppressed++;
269
+ return;
270
+ }
271
+ counts.matched++;
272
+ risk = true;
273
+ if (f.severity === "fatal") fatal = true;
274
+ reasons.push({
275
+ dshVersion: c.dshVersion,
276
+ pluginName,
277
+ findingIndex,
278
+ code: "finding",
279
+ message: `${f.severity}/${f.certainty}: ${f.category}: ${f.impact}`,
280
+ });
281
+ });
282
+ }
283
+ }
284
+ return finish(tool ? 3 : fatal ? 2 : risk ? 1 : 0);
285
+ }
@@ -1,6 +1,9 @@
1
1
  /** Keep the frozen legacy catalogue available for older targets and overrides. */
2
2
  import path from "node:path";
3
3
  import semver from "semver";
4
+ import fs from "node:fs";
5
+ import { createHash } from "node:crypto";
6
+ import { CATALOG_REFERENCES, describeCatalog } from "./contracts.mjs";
4
7
 
5
8
  export function catalogPathFor(root, version, override) {
6
9
  if (override) return override;
@@ -14,3 +17,38 @@ export function catalogPathFor(root, version, override) {
14
17
  modern ? "dsh-api-catalog-0.2.1-rc.2.json" : "dsh-api-catalog.json",
15
18
  );
16
19
  }
20
+
21
+ /** Read once: the reported digest always describes the bytes used to judge. */
22
+ export function loadCatalog(root, version, override) {
23
+ const file = catalogPathFor(root, version, override),
24
+ bytes = fs.readFileSync(file);
25
+ const sha256 = createHash("sha256").update(bytes).digest("hex");
26
+ const known = CATALOG_REFERENCES.find(
27
+ (c) => c.file === path.basename(file) && c.sha256 === sha256,
28
+ );
29
+ if (!override && !known)
30
+ throw new Error(
31
+ "内置 API 目录摘要与已验收记录不符,无法判定;请核对目录来源",
32
+ );
33
+ const catalog = JSON.parse(bytes);
34
+ if (
35
+ !catalog ||
36
+ typeof catalog !== "object" ||
37
+ Array.isArray(catalog) ||
38
+ !catalog.host ||
39
+ !catalog.client
40
+ )
41
+ throw new Error("API 目录格式无效");
42
+ return {
43
+ file,
44
+ catalog,
45
+ evidence: describeCatalog({
46
+ targetVersion: version || null,
47
+ referenceVersion:
48
+ known?.version ||
49
+ (typeof catalog._dshVersion === "string" ? catalog._dshVersion : null),
50
+ sha256,
51
+ override: Boolean(override),
52
+ }),
53
+ };
54
+ }
@@ -8,6 +8,61 @@
8
8
  export const REPORT_SCHEMA = 1;
9
9
  export const REPORT_KIND = "plugin-compat-check/result";
10
10
  export const ENVELOPE_KIND = "plugin-compat-check/report";
11
+ /** Reviewed catalogue bytes, not a promise of plugin business compatibility.
12
+ * Adding a version requires source verification, rule review and integration
13
+ * evidence; extraction alone must never extend this list. */
14
+ export const CATALOG_REFERENCES = Object.freeze([
15
+ Object.freeze({
16
+ version: "0.2.0-rc.2",
17
+ file: "dsh-api-catalog.json",
18
+ sha256: "1331bf3220c1e06cfe087237077118d002dc04c32d027ce01da74c6a0d1dc9ad",
19
+ }),
20
+ Object.freeze({
21
+ version: "0.2.1-rc.2",
22
+ file: "dsh-api-catalog-0.2.1-rc.2.json",
23
+ sha256: "90b4ae484ebc6660c53e5c9ad14ba657581534b30e8222c636c759e19587f743",
24
+ }),
25
+ ]);
26
+ export function describeCatalog({
27
+ targetVersion = null,
28
+ referenceVersion = null,
29
+ sha256,
30
+ override = false,
31
+ }) {
32
+ const known =
33
+ !override &&
34
+ CATALOG_REFERENCES.find(
35
+ (c) => c.version === referenceVersion && c.sha256 === sha256,
36
+ );
37
+ const verified = Boolean(known && targetVersion === referenceVersion);
38
+ return {
39
+ targetVersion,
40
+ referenceVersion,
41
+ sha256,
42
+ selection: override ? "override" : verified ? "exact" : "fallback",
43
+ verified,
44
+ certifiedVersions: known ? [known.version] : [],
45
+ scope:
46
+ "仅核验内置目录字节及对应精确版本的集成;不代表业务验收或运行时观测。",
47
+ warning: verified
48
+ ? null
49
+ : override
50
+ ? "自定义参考目录未认证;目录自报版本或相同字节不扩大已验收范围,请在实际目标宿主复核。"
51
+ : "目标 DSH 版本未知或未认证;普通体检使用参考目录回退,不能据此认定该版本已验收。",
52
+ };
53
+ }
54
+ export function catalogLines(c) {
55
+ if (!c) return ["API 目录身份未提供;版本范围未认证,请重新体检。"];
56
+ return [
57
+ "目标 DSH:" + (c.targetVersion || "未知"),
58
+ "参考目录:" + (c.referenceVersion || "未知") + " · SHA-256:" + c.sha256,
59
+ "目录认证范围:" +
60
+ (c.certifiedVersions.join("、") || "无") +
61
+ ";" +
62
+ c.scope,
63
+ ...(c.warning ? [c.warning] : []),
64
+ ];
65
+ }
11
66
  const record = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
12
67
  const text = (v) => typeof v === "string" && v.length > 0;
13
68
  const nullableText = (v) => v === null || typeof v === "string";
@@ -282,6 +337,37 @@ export function validateReportBody(body) {
282
337
  }
283
338
  need(e, body.error === null, "report.error");
284
339
  need(e, nullableText(body.dshVersion), "report.dshVersion");
340
+ // Older schema-1 reports remain readable, but consumers show missing identity.
341
+ if (body.catalog !== undefined) {
342
+ const c = body.catalog;
343
+ if (
344
+ !record(c) ||
345
+ !nullableText(c.referenceVersion) ||
346
+ !/^[a-f0-9]{64}$/.test(c.sha256)
347
+ )
348
+ e.push("report.catalog");
349
+ else {
350
+ const expected = describeCatalog({
351
+ targetVersion: body.dshVersion,
352
+ referenceVersion: c.referenceVersion,
353
+ sha256: c.sha256,
354
+ override: c.selection === "override",
355
+ });
356
+ if (
357
+ c.selection !== "override" &&
358
+ !CATALOG_REFERENCES.some(
359
+ (k) => k.version === c.referenceVersion && k.sha256 === c.sha256,
360
+ )
361
+ )
362
+ e.push("report.catalog.identity");
363
+ for (const [key, value] of Object.entries(expected))
364
+ need(
365
+ e,
366
+ JSON.stringify(c[key]) === JSON.stringify(value),
367
+ "report.catalog." + key,
368
+ );
369
+ }
370
+ }
285
371
  e.push(...validateObservation(body.observation));
286
372
  if (
287
373
  record(body.observation) &&
@@ -466,6 +552,8 @@ export function validateEnvelope(data) {
466
552
  )
467
553
  return ["envelope.schemaVersion/kind"];
468
554
  const e = validateReportBody(data.report);
555
+ if (Object.hasOwn(data, "issueReport"))
556
+ need(e, typeof data.issueReport === "string", "envelope.issueReport");
469
557
  need(
470
558
  e,
471
559
  typeof data.ok === "boolean" && data.ok === data.report?.ok,