@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,297 @@
1
+ import { existsSync } from "node:fs";
2
+ import { readdir, readFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { renderPaddedMarkdownTable, writeJsonMarkdownArtifacts } from "./artifacts.js";
5
+ import { resolveFromRoot, toRepoPath } from "./path-utils.js";
6
+
7
+ export const defaultExecutionResultsOptions = {
8
+ generatedAt: "deterministic",
9
+ markdownPath: "reports/plugin-execution-results.md",
10
+ reportTitle: "Plugin Execution Results",
11
+ resultsDir: ".plugin-inspector/results",
12
+ jsonPath: "reports/plugin-execution-results.json",
13
+ };
14
+
15
+ export async function buildExecutionResultsReport(options = {}) {
16
+ const rootDir = path.resolve(options.rootDir ?? process.cwd());
17
+ const resultsDir = resolveFromRoot(rootDir, options.resultsDir ?? defaultExecutionResultsOptions.resultsDir);
18
+ const artifacts = existsSync(resultsDir) ? await readArtifacts(resultsDir, { rootDir }) : [];
19
+ const syntheticArtifacts = artifacts.filter((artifact) => artifact.kind === "synthetic");
20
+ const captureArtifacts = artifacts.filter((artifact) => artifact.kind === "capture");
21
+ const auditArtifacts = artifacts.filter((artifact) => artifact.kind === "audit");
22
+ const profileArtifacts = artifacts.filter((artifact) => artifact.kind === "profile");
23
+
24
+ return {
25
+ generatedAt: options.generatedAt ?? defaultExecutionResultsOptions.generatedAt,
26
+ resultsDir: repoRelative(resultsDir, { rootDir }),
27
+ summary: {
28
+ artifactCount: artifacts.length,
29
+ captureArtifactCount: captureArtifacts.length,
30
+ syntheticArtifactCount: syntheticArtifacts.length,
31
+ auditArtifactCount: auditArtifacts.length,
32
+ profileArtifactCount: profileArtifacts.length,
33
+ capturedRegistrationCount: captureArtifacts.reduce(
34
+ (sum, artifact) => sum + (artifact.capturedCount ?? 0),
35
+ 0,
36
+ ),
37
+ auditFindingCount: auditArtifacts.reduce((sum, artifact) => sum + artifact.findingCount, 0),
38
+ executionWallMs: profileArtifacts.reduce((sum, artifact) => sum + (artifact.summary?.totalWallMs ?? 0), 0),
39
+ maxPeakRssMb: Math.max(0, ...profileArtifacts.map((artifact) => artifact.summary?.maxPeakRssMb ?? 0)),
40
+ maxCpuMsEstimate: Math.max(0, ...profileArtifacts.map((artifact) => artifact.summary?.maxCpuMsEstimate ?? 0)),
41
+ passCount: syntheticArtifacts.reduce((sum, artifact) => sum + (artifact.summary?.passCount ?? 0), 0),
42
+ failCount: syntheticArtifacts.reduce((sum, artifact) => sum + (artifact.summary?.failCount ?? 0), 0),
43
+ blockedCount: syntheticArtifacts.reduce((sum, artifact) => sum + (artifact.summary?.blockedCount ?? 0), 0),
44
+ },
45
+ artifacts,
46
+ };
47
+ }
48
+
49
+ export async function writeExecutionResultsReport(report, options = {}) {
50
+ const rootDir = path.resolve(options.rootDir ?? process.cwd());
51
+ const jsonPath = resolveFromRoot(rootDir, options.jsonPath ?? defaultExecutionResultsOptions.jsonPath);
52
+ const markdownPath = resolveFromRoot(rootDir, options.markdownPath ?? defaultExecutionResultsOptions.markdownPath);
53
+ return writeJsonMarkdownArtifacts({
54
+ jsonPath,
55
+ markdownPath,
56
+ json: report,
57
+ markdown: renderExecutionResultsMarkdown(report, options),
58
+ check: options.check,
59
+ });
60
+ }
61
+
62
+ export function renderExecutionResultsMarkdown(report, options = {}) {
63
+ const title = options.title ?? options.reportTitle ?? defaultExecutionResultsOptions.reportTitle;
64
+ return [
65
+ `# ${title}`,
66
+ "",
67
+ `Generated: ${report.generatedAt}`,
68
+ `Results dir: ${report.resultsDir}`,
69
+ "",
70
+ "## Summary",
71
+ "",
72
+ markdownTable(
73
+ [
74
+ ["Artifacts", report.summary.artifactCount],
75
+ ["Capture artifacts", report.summary.captureArtifactCount],
76
+ ["Synthetic artifacts", report.summary.syntheticArtifactCount],
77
+ ["Audit artifacts", report.summary.auditArtifactCount],
78
+ ["Profile artifacts", report.summary.profileArtifactCount],
79
+ ["Captured registrations/hooks", report.summary.capturedRegistrationCount],
80
+ ["Audit findings", report.summary.auditFindingCount],
81
+ ["Execution wall", `${report.summary.executionWallMs} ms`],
82
+ ["Max peak RSS", `${report.summary.maxPeakRssMb} MB`],
83
+ ["Max CPU estimate", `${report.summary.maxCpuMsEstimate} ms`],
84
+ ["Pass", report.summary.passCount],
85
+ ["Fail", report.summary.failCount],
86
+ ["Blocked", report.summary.blockedCount],
87
+ ],
88
+ ["Metric", "Value"],
89
+ ),
90
+ "",
91
+ "## Artifacts",
92
+ "",
93
+ markdownTable(
94
+ report.artifacts.map((artifact) => [
95
+ artifact.fixture,
96
+ artifact.kind,
97
+ artifact.status,
98
+ artifact.entrypoint,
99
+ summarizeArtifactResult(artifact),
100
+ artifact.artifactPath,
101
+ ]),
102
+ ["Fixture", "Kind", "Status", "Entrypoint", "Result", "Artifact"],
103
+ ),
104
+ "",
105
+ "## Blocked Synthetic Probes",
106
+ "",
107
+ markdownTable(
108
+ report.artifacts.flatMap((artifact) =>
109
+ (artifact.blocked ?? []).map((item) => [
110
+ artifact.fixture,
111
+ item.kind,
112
+ item.seam,
113
+ item.label,
114
+ item.reason,
115
+ artifact.artifactPath,
116
+ ]),
117
+ ),
118
+ ["Fixture", "Kind", "Seam", "Label", "Reason", "Artifact"],
119
+ ),
120
+ "",
121
+ "## Failed Synthetic Probes",
122
+ "",
123
+ markdownTable(
124
+ report.artifacts.flatMap((artifact) =>
125
+ (artifact.failures ?? []).map((item) => [
126
+ artifact.fixture,
127
+ item.kind,
128
+ item.seam,
129
+ item.label,
130
+ item.error,
131
+ artifact.artifactPath,
132
+ ]),
133
+ ),
134
+ ["Fixture", "Kind", "Seam", "Label", "Error", "Artifact"],
135
+ ),
136
+ "",
137
+ "## Dependency Audit Artifacts",
138
+ "",
139
+ markdownTable(
140
+ report.artifacts
141
+ .filter((artifact) => artifact.kind === "audit")
142
+ .map((artifact) => [
143
+ artifact.fixture,
144
+ artifact.findingCount,
145
+ artifact.vulnerabilities ? JSON.stringify(artifact.vulnerabilities) : "-",
146
+ artifact.artifactPath,
147
+ ]),
148
+ ["Fixture", "Findings", "Vulnerabilities", "Artifact"],
149
+ ),
150
+ "",
151
+ "## Execution Profiles",
152
+ "",
153
+ markdownTable(
154
+ report.artifacts.flatMap((artifact) =>
155
+ (artifact.slowestSteps ?? []).map((step) => [
156
+ artifact.fixture,
157
+ step.kind,
158
+ `${step.wallMs} ms`,
159
+ `${step.peakRssMb} MB`,
160
+ `${step.cpuMsEstimate} ms`,
161
+ step.command,
162
+ ]),
163
+ ),
164
+ ["Fixture", "Step", "Wall", "Peak RSS", "CPU Estimate", "Command"],
165
+ ),
166
+ ].join("\n");
167
+ }
168
+
169
+ async function readArtifacts(resultsDir, options) {
170
+ const paths = await listJsonFiles(resultsDir);
171
+ const artifacts = [];
172
+ for (const artifactPath of paths) {
173
+ const parsed = JSON.parse(await readFile(artifactPath, "utf8"));
174
+ const relativePath = repoRelative(artifactPath, options);
175
+ artifacts.push(summarizeArtifact({ artifactPath: relativePath, parsed, rootDir: options.rootDir }));
176
+ }
177
+ return artifacts.sort((left, right) => left.artifactPath.localeCompare(right.artifactPath));
178
+ }
179
+
180
+ async function listJsonFiles(dir) {
181
+ const entries = await readdir(dir, { withFileTypes: true });
182
+ const files = [];
183
+ for (const entry of entries) {
184
+ const entryPath = path.join(dir, entry.name);
185
+ if (entry.isDirectory()) {
186
+ files.push(...(await listJsonFiles(entryPath)));
187
+ continue;
188
+ }
189
+ if (entry.isFile() && entry.name.endsWith(".json")) {
190
+ files.push(entryPath);
191
+ }
192
+ }
193
+ return files;
194
+ }
195
+
196
+ function summarizeArtifact({ artifactPath, parsed, rootDir }) {
197
+ const normalizedArtifactPath = toRepoPath(artifactPath);
198
+ const kind = normalizedArtifactPath.endsWith(".synthetic.json")
199
+ ? "synthetic"
200
+ : normalizedArtifactPath.endsWith("package-audit.json")
201
+ ? "audit"
202
+ : normalizedArtifactPath.endsWith("execution-profile.json")
203
+ ? "profile"
204
+ : "capture";
205
+ const fixture = normalizedArtifactPath.split("/").at(-2) ?? "unknown";
206
+ if (kind === "synthetic") {
207
+ return {
208
+ artifactPath: normalizedArtifactPath,
209
+ fixture,
210
+ kind,
211
+ entrypoint: scrubPath(parsed.entrypoint, { rootDir }),
212
+ status: parsed.status,
213
+ summary: parsed.summary,
214
+ failures: (parsed.results ?? []).filter((result) => result.status === "fail"),
215
+ blocked: (parsed.results ?? []).filter((result) => result.status === "blocked"),
216
+ };
217
+ }
218
+ if (kind === "audit") {
219
+ return {
220
+ artifactPath: normalizedArtifactPath,
221
+ fixture,
222
+ kind,
223
+ entrypoint: "-",
224
+ status: "warning",
225
+ findingCount: auditFindingCount(parsed),
226
+ vulnerabilities: parsed.metadata?.vulnerabilities ?? null,
227
+ };
228
+ }
229
+ if (kind === "profile") {
230
+ return {
231
+ artifactPath: normalizedArtifactPath,
232
+ fixture,
233
+ kind,
234
+ entrypoint: "-",
235
+ status: parsed.summary?.failCount > 0 ? "fail" : "pass",
236
+ summary: parsed.summary,
237
+ slowestSteps: [...(parsed.steps ?? [])].sort((left, right) => right.wallMs - left.wallMs).slice(0, 5),
238
+ };
239
+ }
240
+ return {
241
+ artifactPath: normalizedArtifactPath,
242
+ fixture,
243
+ kind,
244
+ entrypoint: scrubPath(parsed.entrypoint, { rootDir }),
245
+ status: parsed.status,
246
+ capturedCount: parsed.captured?.length ?? 0,
247
+ captured: (parsed.captured ?? []).map((item) => `${item.kind}:${item.name}`),
248
+ };
249
+ }
250
+
251
+ function summarizeArtifactResult(artifact) {
252
+ if (artifact.kind === "audit") {
253
+ return `${artifact.findingCount} audit findings`;
254
+ }
255
+ if (artifact.kind === "profile") {
256
+ return `${artifact.summary?.stepCount ?? 0} steps / ${artifact.summary?.totalWallMs ?? 0} ms / ${artifact.summary?.maxPeakRssMb ?? 0} MB`;
257
+ }
258
+ if (artifact.summary) {
259
+ return `${artifact.summary.passCount} pass / ${artifact.summary.failCount} fail / ${artifact.summary.blockedCount} blocked`;
260
+ }
261
+ return `${artifact.capturedCount} captured`;
262
+ }
263
+
264
+ function auditFindingCount(parsed) {
265
+ const vulnerabilities = parsed.metadata?.vulnerabilities;
266
+ if (vulnerabilities && typeof vulnerabilities === "object") {
267
+ const severityTotal = Object.entries(vulnerabilities)
268
+ .filter(([key]) => key !== "total")
269
+ .reduce((sum, [, value]) => sum + (Number(value) || 0), 0);
270
+ return severityTotal || Number(vulnerabilities.total) || 0;
271
+ }
272
+ if (Array.isArray(parsed.vulnerabilities)) {
273
+ return parsed.vulnerabilities.length;
274
+ }
275
+ if (parsed.vulnerabilities && typeof parsed.vulnerabilities === "object") {
276
+ return Object.keys(parsed.vulnerabilities).length;
277
+ }
278
+ return 0;
279
+ }
280
+
281
+ function scrubPath(value, options) {
282
+ return typeof value === "string" ? repoRelative(value, options) : value;
283
+ }
284
+
285
+ function repoRelative(value, options = {}) {
286
+ const rootDir = path.resolve(options.rootDir ?? process.cwd());
287
+ const absolute = path.resolve(value);
288
+ const relative = path.relative(rootDir, absolute);
289
+ if (relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative))) {
290
+ return toRepoPath(relative || ".");
291
+ }
292
+ return toRepoPath(value);
293
+ }
294
+
295
+ function markdownTable(rows, headers) {
296
+ return renderPaddedMarkdownTable(rows, headers);
297
+ }