@openclaw/plugin-inspector 0.0.0 → 0.1.1

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.
@@ -0,0 +1,335 @@
1
+ import { renderMarkdownTable, writeJsonMarkdownArtifacts } from "./artifacts.js";
2
+
3
+ export const defaultRefDiffOptions = {
4
+ generatedAt: "deterministic",
5
+ jsonPath: "reports/plugin-ref-diff.json",
6
+ markdownPath: "reports/plugin-ref-diff.md",
7
+ reportTitle: "Plugin Inspector Ref Diff",
8
+ };
9
+
10
+ export const defaultRefDiffDimensions = [
11
+ {
12
+ id: "compatRecords",
13
+ label: "Compat records",
14
+ targetKey: "compatRecords",
15
+ used: () => new Set(),
16
+ hardWhenRemovedAndUsed: false,
17
+ },
18
+ {
19
+ id: "hookNames",
20
+ label: "Hook names",
21
+ targetKey: "hookNames",
22
+ used: (report) => new Set(report.fixtures.flatMap((fixture) => fixture.hooks)),
23
+ hardWhenRemovedAndUsed: true,
24
+ },
25
+ {
26
+ id: "apiRegistrars",
27
+ label: "API registrars",
28
+ targetKey: "apiRegistrars",
29
+ used: (report) => new Set(report.fixtures.flatMap((fixture) => fixture.registrations)),
30
+ hardWhenRemovedAndUsed: true,
31
+ },
32
+ {
33
+ id: "capturedRegistrars",
34
+ label: "Captured registrars",
35
+ targetKey: "capturedRegistrars",
36
+ used: (report) => new Set(report.fixtures.flatMap((fixture) => fixture.registrations)),
37
+ hardWhenRemovedAndUsed: false,
38
+ },
39
+ {
40
+ id: "sdkExports",
41
+ label: "SDK exports",
42
+ targetKey: "sdkExports",
43
+ used: (report) => new Set(report.fixtures.flatMap((fixture) => fixture.sdkImports)),
44
+ hardWhenRemovedAndUsed: true,
45
+ },
46
+ {
47
+ id: "manifestFields",
48
+ label: "Manifest fields",
49
+ targetKey: "manifestFields",
50
+ used: (report) =>
51
+ new Set(
52
+ report.fixtures.flatMap((fixture) =>
53
+ fixture.pluginManifests.flatMap((pluginManifest) => Object.keys(pluginManifest)),
54
+ ),
55
+ ),
56
+ hardWhenRemovedAndUsed: true,
57
+ },
58
+ {
59
+ id: "manifestContractFields",
60
+ label: "Manifest contract fields",
61
+ targetKey: "manifestContractFields",
62
+ used: (report) => new Set(report.fixtures.flatMap((fixture) => fixture.manifestContracts)),
63
+ hardWhenRemovedAndUsed: true,
64
+ },
65
+ ];
66
+
67
+ export async function buildRefDiff(options = {}) {
68
+ const baseReport = options.baseReport;
69
+ const headReport = options.headReport;
70
+ if (!baseReport || !headReport) {
71
+ throw new TypeError("buildRefDiff requires baseReport and headReport");
72
+ }
73
+
74
+ const dimensions = (options.dimensions ?? defaultRefDiffDimensions).map((dimension) =>
75
+ compareDimension(dimension, baseReport, headReport),
76
+ );
77
+ const issueDelta = compareIssues(baseReport, headReport);
78
+ const regressions = [
79
+ ...dimensions.flatMap((dimension) => dimension.regressions),
80
+ ...issueDelta.regressions,
81
+ ...targetStatusRegressions(baseReport, headReport),
82
+ ].sort((left, right) => severityRank(left.severity) - severityRank(right.severity) || left.code.localeCompare(right.code));
83
+
84
+ const hardRegressionCount = regressions.filter((regression) => regression.action === "fail").length;
85
+ const warningRegressionCount = regressions.filter((regression) => regression.action === "warn").length;
86
+
87
+ return {
88
+ generatedAt: options.generatedAt ?? defaultRefDiffOptions.generatedAt,
89
+ base: summarizeReport(options.baseLabel ?? "base", baseReport),
90
+ head: summarizeReport(options.headLabel ?? "head", headReport),
91
+ status: hardRegressionCount === 0 ? "pass" : "fail",
92
+ summary: {
93
+ dimensionCount: dimensions.length,
94
+ hardRegressionCount,
95
+ warningRegressionCount,
96
+ newIssueCount: issueDelta.added.length,
97
+ newP0IssueCount: issueDelta.added.filter((issue) => issue.severity === "P0").length,
98
+ newP1IssueCount: issueDelta.added.filter((issue) => issue.severity === "P1").length,
99
+ removedIssueCount: issueDelta.removed.length,
100
+ },
101
+ dimensions,
102
+ issueDelta,
103
+ regressions,
104
+ };
105
+ }
106
+
107
+ export function validateRefDiff(diff, options = {}) {
108
+ return diff.regressions
109
+ .filter((regression) => regression.action === "fail" || (options.strict && regression.action === "warn"))
110
+ .map((regression) => `${regression.code}: ${regression.message}: ${regression.evidence.join(", ")}`);
111
+ }
112
+
113
+ export async function writeRefDiff(diff, options = {}) {
114
+ return writeJsonMarkdownArtifacts({
115
+ jsonPath: options.jsonPath,
116
+ markdownPath: options.markdownPath,
117
+ json: diff,
118
+ markdown: renderRefDiffMarkdown(diff, options),
119
+ check: options.check,
120
+ });
121
+ }
122
+
123
+ export function renderRefDiffMarkdown(diff, options = {}) {
124
+ const title = options.title ?? options.reportTitle ?? defaultRefDiffOptions.reportTitle;
125
+ return [
126
+ `# ${title}`,
127
+ "",
128
+ `Generated: ${diff.generatedAt}`,
129
+ `Status: ${diff.status.toUpperCase()}`,
130
+ "",
131
+ "## Summary",
132
+ "",
133
+ markdownTable(
134
+ [
135
+ ["Base", `${diff.base.label} (${diff.base.targetOpenClaw.status})`],
136
+ ["Head", `${diff.head.label} (${diff.head.targetOpenClaw.status})`],
137
+ ["Hard regressions", diff.summary.hardRegressionCount],
138
+ ["Warning regressions", diff.summary.warningRegressionCount],
139
+ ["New issues", diff.summary.newIssueCount],
140
+ ["New P0 issues", diff.summary.newP0IssueCount],
141
+ ["New P1 issues", diff.summary.newP1IssueCount],
142
+ ["Removed issues", diff.summary.removedIssueCount],
143
+ ],
144
+ ["Metric", "Value"],
145
+ ),
146
+ "",
147
+ "## Target Surface Delta",
148
+ "",
149
+ markdownTable(
150
+ diff.dimensions.map((dimension) => [
151
+ dimension.label,
152
+ dimension.baseCount,
153
+ dimension.headCount,
154
+ signed(dimension.headCount - dimension.baseCount),
155
+ dimension.added.join(", ") || "-",
156
+ dimension.removed.join(", ") || "-",
157
+ dimension.removedUsed.join(", ") || "-",
158
+ ]),
159
+ ["Surface", "Base", "Head", "Delta", "Added", "Removed", "Removed used"],
160
+ ),
161
+ "",
162
+ "## Regressions",
163
+ "",
164
+ markdownTable(
165
+ diff.regressions.map((regression) => [
166
+ regression.action,
167
+ regression.severity,
168
+ regression.dimension,
169
+ regression.code,
170
+ regression.message,
171
+ regression.evidence.join(", "),
172
+ ]),
173
+ ["Action", "Severity", "Surface", "Code", "Message", "Evidence"],
174
+ ),
175
+ "",
176
+ "## New Issues",
177
+ "",
178
+ markdownTable(
179
+ diff.issueDelta.added.map((issue) => [
180
+ issue.severity,
181
+ issue.fixture,
182
+ issue.code,
183
+ issue.title,
184
+ issue.evidence.join(", "),
185
+ ]),
186
+ ["Severity", "Fixture", "Code", "Title", "Evidence"],
187
+ ),
188
+ "",
189
+ "## Removed Issues",
190
+ "",
191
+ markdownTable(
192
+ diff.issueDelta.removed.map((issue) => [
193
+ issue.severity,
194
+ issue.fixture,
195
+ issue.code,
196
+ issue.title,
197
+ issue.evidence.join(", "),
198
+ ]),
199
+ ["Severity", "Fixture", "Code", "Title", "Evidence"],
200
+ ),
201
+ ].join("\n");
202
+ }
203
+
204
+ function summarizeReport(label, report) {
205
+ return {
206
+ label,
207
+ targetOpenClaw: {
208
+ status: report.targetOpenClaw.status,
209
+ configuredPath: report.targetOpenClaw.configuredPath,
210
+ compatRecordCount: report.targetOpenClaw.compatRecordCount ?? 0,
211
+ hookNameCount: report.targetOpenClaw.hookNameCount ?? 0,
212
+ apiRegistrarCount: report.targetOpenClaw.apiRegistrarCount ?? 0,
213
+ capturedRegistrarCount: report.targetOpenClaw.capturedRegistrarCount ?? 0,
214
+ sdkExportCount: report.targetOpenClaw.sdkExportCount ?? 0,
215
+ manifestFieldCount: report.targetOpenClaw.manifestFieldCount ?? 0,
216
+ manifestContractFieldCount: report.targetOpenClaw.manifestContractFieldCount ?? 0,
217
+ },
218
+ report: {
219
+ status: report.status,
220
+ breakageCount: report.summary.breakageCount,
221
+ warningCount: report.summary.warningCount,
222
+ suggestionCount: report.summary.suggestionCount,
223
+ issueCount: report.summary.issueCount,
224
+ p0IssueCount: report.summary.p0IssueCount,
225
+ p1IssueCount: report.summary.p1IssueCount,
226
+ contractProbeCount: report.summary.contractProbeCount,
227
+ },
228
+ };
229
+ }
230
+
231
+ function compareDimension(dimension, baseReport, headReport) {
232
+ const baseValues = sortedSet(baseReport.targetOpenClaw[dimension.targetKey] ?? []);
233
+ const headValues = sortedSet(headReport.targetOpenClaw[dimension.targetKey] ?? []);
234
+ const usedValues = sortedSet([...dimension.used(baseReport), ...dimension.used(headReport)]);
235
+ const added = headValues.filter((value) => !baseValues.includes(value));
236
+ const removed = baseValues.filter((value) => !headValues.includes(value));
237
+ const removedUsed = removed.filter((value) => usedValues.includes(value));
238
+ const regressions = [];
239
+
240
+ if (removedUsed.length > 0) {
241
+ regressions.push({
242
+ code: `${dimension.id}.removed-used`,
243
+ severity: "P0",
244
+ action: dimension.hardWhenRemovedAndUsed ? "fail" : "warn",
245
+ dimension: dimension.id,
246
+ message: `${dimension.label} removed values used by fixtures`,
247
+ evidence: removedUsed,
248
+ });
249
+ }
250
+
251
+ const removedUnused = removed.filter((value) => !usedValues.includes(value));
252
+ if (removedUnused.length > 0) {
253
+ regressions.push({
254
+ code: `${dimension.id}.removed-unused`,
255
+ severity: "P3",
256
+ action: "warn",
257
+ dimension: dimension.id,
258
+ message: `${dimension.label} removed values not used by current fixtures`,
259
+ evidence: removedUnused,
260
+ });
261
+ }
262
+
263
+ return {
264
+ id: dimension.id,
265
+ label: dimension.label,
266
+ baseCount: baseValues.length,
267
+ headCount: headValues.length,
268
+ added,
269
+ removed,
270
+ removedUsed,
271
+ used: usedValues,
272
+ regressions,
273
+ };
274
+ }
275
+
276
+ function compareIssues(baseReport, headReport) {
277
+ const baseIssues = new Map(baseReport.issues.map((issue) => [issue.id, issue]));
278
+ const headIssues = new Map(headReport.issues.map((issue) => [issue.id, issue]));
279
+ const added = [...headIssues.values()].filter((issue) => !baseIssues.has(issue.id)).sort(issueSort);
280
+ const removed = [...baseIssues.values()].filter((issue) => !headIssues.has(issue.id)).sort(issueSort);
281
+ const regressions = added.map((issue) => ({
282
+ code: `issue.${issue.id}`,
283
+ severity: issue.severity,
284
+ action: ["P0", "P1"].includes(issue.severity) ? "fail" : "warn",
285
+ dimension: "issues",
286
+ message: `new ${issue.severity} issue: ${issue.title}`,
287
+ evidence: [issue.fixture, issue.code, ...issue.evidence],
288
+ }));
289
+
290
+ return { added, removed, regressions };
291
+ }
292
+
293
+ function targetStatusRegressions(baseReport, headReport) {
294
+ if (baseReport.targetOpenClaw.status === headReport.targetOpenClaw.status) {
295
+ return [];
296
+ }
297
+ return [
298
+ {
299
+ code: "target.status.changed",
300
+ severity: headReport.targetOpenClaw.status === "ok" ? "P3" : "P0",
301
+ action: headReport.targetOpenClaw.status === "ok" ? "warn" : "fail",
302
+ dimension: "targetOpenClaw",
303
+ message: `target OpenClaw status changed from ${baseReport.targetOpenClaw.status} to ${headReport.targetOpenClaw.status}`,
304
+ evidence: [
305
+ baseReport.targetOpenClaw.configuredPath ?? "base target path unknown",
306
+ headReport.targetOpenClaw.configuredPath ?? "head target path unknown",
307
+ ],
308
+ },
309
+ ];
310
+ }
311
+
312
+ function sortedSet(values) {
313
+ return [...new Set(values.filter(Boolean))].sort();
314
+ }
315
+
316
+ function issueSort(left, right) {
317
+ return severityRank(left.severity) - severityRank(right.severity) || left.id.localeCompare(right.id);
318
+ }
319
+
320
+ function severityRank(value) {
321
+ return { P0: 0, P1: 1, P2: 2, P3: 3 }[value] ?? 4;
322
+ }
323
+
324
+ function signed(value) {
325
+ return value > 0 ? `+${value}` : String(value);
326
+ }
327
+
328
+ function markdownTable(rows, headers) {
329
+ return renderMarkdownTable(rows, headers, {
330
+ empty: "_none_",
331
+ escape: false,
332
+ nullValue: "-",
333
+ padding: true,
334
+ });
335
+ }