@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,356 @@
1
+ import { renderPaddedMarkdownTable } from "./artifacts.js";
2
+
3
+ const defaultSeverityLabels = {
4
+ P0: "P0",
5
+ P1: "P1",
6
+ P2: "P2",
7
+ P3: "P3",
8
+ };
9
+
10
+ export function renderCompatibilityMarkdownReport(report, options = {}) {
11
+ return [
12
+ `# ${options.title ?? "OpenClaw Plugin Compatibility Report"}`,
13
+ "",
14
+ `Generated: ${report.generatedAt}`,
15
+ `Status: ${report.status.toUpperCase()}`,
16
+ "",
17
+ "## Summary",
18
+ "",
19
+ markdownTable(
20
+ [
21
+ ["Fixtures", report.summary.fixtureCount],
22
+ ["High-priority fixtures", report.summary.highPriorityFixtures],
23
+ ["Hard breakages", report.summary.breakageCount],
24
+ ["Warnings", report.summary.warningCount],
25
+ ["Compatibility suggestions", report.summary.suggestionCount],
26
+ ["Issue findings", report.summary.issueCount],
27
+ ["P0 issues", report.summary.p0IssueCount],
28
+ ["P1 issues", report.summary.p1IssueCount],
29
+ ["Live issues", report.summary.liveIssueCount],
30
+ ["Live P0 issues", report.summary.liveP0IssueCount],
31
+ ["Compat gaps", report.summary.compatGapCount],
32
+ ["Deprecation warnings", report.summary.deprecationWarningCount],
33
+ ["Inspector gaps", report.summary.inspectorGapCount],
34
+ ["Upstream metadata", report.summary.upstreamIssueCount],
35
+ ["Contract probes", report.summary.contractProbeCount],
36
+ ["Decision rows", report.summary.decisionCount],
37
+ ],
38
+ ["Metric", "Value"],
39
+ ),
40
+ "",
41
+ "## Triage Overview",
42
+ "",
43
+ triageOverview(report),
44
+ "",
45
+ "## P0 Live Issues",
46
+ "",
47
+ issuesTable(
48
+ report.issues.filter((issue) => issue.issueClass === "live-issue" && issue.severity === "P0"),
49
+ options,
50
+ ),
51
+ "",
52
+ "## Live Issues",
53
+ "",
54
+ issuesTable(report.issues.filter((issue) => issue.issueClass === "live-issue"), options),
55
+ "",
56
+ "## Compat Gaps",
57
+ "",
58
+ issuesTable(report.issues.filter((issue) => issue.issueClass === "compat-gap"), options),
59
+ "",
60
+ "## Deprecation Warnings",
61
+ "",
62
+ issuesTable(report.issues.filter((issue) => issue.issueClass === "deprecation-warning"), options),
63
+ "",
64
+ "## Inspector Proof Gaps",
65
+ "",
66
+ issuesTable(report.issues.filter((issue) => issue.issueClass === "inspector-gap"), options),
67
+ "",
68
+ "## Upstream Metadata Issues",
69
+ "",
70
+ issuesTable(report.issues.filter((issue) => issue.issueClass === "upstream-metadata"), options),
71
+ "",
72
+ "## Hard Breakages",
73
+ "",
74
+ findingsTable(report.breakages),
75
+ "",
76
+ "## Target OpenClaw Compat Records",
77
+ "",
78
+ targetOpenClawTable(report.targetOpenClaw),
79
+ "",
80
+ "## Warnings",
81
+ "",
82
+ findingsTable(report.warnings),
83
+ "",
84
+ "## Suggestions To OpenClaw Compat Layer",
85
+ "",
86
+ findingsTable(report.suggestions),
87
+ "",
88
+ "## Issue Findings",
89
+ "",
90
+ issuesTable(report.issues, options),
91
+ "",
92
+ "## Contract Probe Backlog",
93
+ "",
94
+ contractProbesTable(report.contractProbes, options),
95
+ "",
96
+ "## Fixture Seam Inventory",
97
+ "",
98
+ markdownTable(
99
+ report.fixtures.map((fixture) => [
100
+ fixture.id,
101
+ fixture.priority,
102
+ fixture.seams.join(", "),
103
+ fixture.hooks.join(", ") || "-",
104
+ fixture.registrations.join(", ") || "-",
105
+ fixture.manifestContracts.join(", ") || "-",
106
+ ]),
107
+ ["Fixture", "Priority", "Seams", "Hooks", "Registrations", "Manifest contracts"],
108
+ ),
109
+ "",
110
+ "## Decision Matrix",
111
+ "",
112
+ markdownTable(
113
+ report.decisions.map((decision) => [
114
+ decision.fixture,
115
+ decision.decision,
116
+ decision.seam,
117
+ decision.action,
118
+ decision.evidence,
119
+ ]),
120
+ ["Fixture", "Decision", "Seam", "Action", "Evidence"],
121
+ ),
122
+ "",
123
+ "## Raw Logs",
124
+ "",
125
+ findingsTable(report.logs),
126
+ ].join("\n");
127
+ }
128
+
129
+ export function renderCompatibilityIssuesReport(report, options = {}) {
130
+ return [
131
+ `# ${options.title ?? "OpenClaw Plugin Issue Findings"}`,
132
+ "",
133
+ `Generated: ${report.generatedAt}`,
134
+ `Status: ${report.status.toUpperCase()}`,
135
+ "",
136
+ "## Triage Summary",
137
+ "",
138
+ markdownTable(
139
+ [
140
+ ["Issue findings", report.summary.issueCount],
141
+ [severityLabel("P0", options), report.summary.p0IssueCount],
142
+ [severityLabel("P1", options), report.summary.p1IssueCount],
143
+ ["Live issues", report.summary.liveIssueCount],
144
+ ["Live P0 issues", report.summary.liveP0IssueCount],
145
+ ["Compat gaps", report.summary.compatGapCount],
146
+ ["Deprecation warnings", report.summary.deprecationWarningCount],
147
+ ["Inspector gaps", report.summary.inspectorGapCount],
148
+ ["Upstream metadata", report.summary.upstreamIssueCount],
149
+ ["Contract probes", report.summary.contractProbeCount],
150
+ ],
151
+ ["Metric", "Value"],
152
+ ),
153
+ "",
154
+ "## Triage Overview",
155
+ "",
156
+ triageOverview(report),
157
+ "",
158
+ "## P0 Live Issues",
159
+ "",
160
+ issuesTable(
161
+ report.issues.filter((issue) => issue.issueClass === "live-issue" && issue.severity === "P0"),
162
+ options,
163
+ ),
164
+ "",
165
+ "## Live Issues",
166
+ "",
167
+ issuesTable(report.issues.filter((issue) => issue.issueClass === "live-issue"), options),
168
+ "",
169
+ "## Compat Gaps",
170
+ "",
171
+ issuesTable(report.issues.filter((issue) => issue.issueClass === "compat-gap"), options),
172
+ "",
173
+ "## Deprecation Warnings",
174
+ "",
175
+ issuesTable(report.issues.filter((issue) => issue.issueClass === "deprecation-warning"), options),
176
+ "",
177
+ "## Inspector Proof Gaps",
178
+ "",
179
+ issuesTable(report.issues.filter((issue) => issue.issueClass === "inspector-gap"), options),
180
+ "",
181
+ "## Upstream Metadata Issues",
182
+ "",
183
+ issuesTable(report.issues.filter((issue) => issue.issueClass === "upstream-metadata"), options),
184
+ "",
185
+ "## Issues",
186
+ "",
187
+ issuesTable(report.issues, options),
188
+ "",
189
+ "## Contract Probe Backlog",
190
+ "",
191
+ contractProbesTable(report.contractProbes, options),
192
+ ].join("\n");
193
+ }
194
+
195
+ function findingsTable(findings) {
196
+ if (findings.length === 0) {
197
+ return "_none_";
198
+ }
199
+
200
+ return markdownTable(
201
+ findings.map((finding) => [
202
+ finding.fixture,
203
+ finding.code,
204
+ finding.level,
205
+ finding.message,
206
+ (finding.evidence ?? []).join(", ") || "-",
207
+ finding.compatRecord ?? "-",
208
+ ]),
209
+ ["Fixture", "Code", "Level", "Message", "Evidence", "Compat record"],
210
+ );
211
+ }
212
+
213
+ function issuesTable(issues, options) {
214
+ if (issues.length === 0) {
215
+ return "_none_";
216
+ }
217
+
218
+ return issues.map((issue) => issueBlock(issue, options)).join("\n\n");
219
+ }
220
+
221
+ function issueBlock(issue, options) {
222
+ return [
223
+ `- ${severityLabel(issue.severity, options)} **${issue.fixture}** \`${issue.issueClass}\` \`${issue.decision}\``,
224
+ ` - **${issue.code}**: ${issue.title}`,
225
+ ` - state: ${issueState(issue)}`,
226
+ " - evidence:",
227
+ ...evidenceList(issue.evidence, options).map((item) => ` - ${item}`),
228
+ ].join("\n");
229
+ }
230
+
231
+ function issueState(issue) {
232
+ const flags = [
233
+ issue.status,
234
+ `compat:${issue.compatStatus ?? "none"}`,
235
+ issue.live ? "live" : null,
236
+ issue.deprecated ? "deprecated" : null,
237
+ ].filter(Boolean);
238
+ return flags.join(" · ");
239
+ }
240
+
241
+ function triageOverview(report) {
242
+ return markdownTable(
243
+ [
244
+ [
245
+ "live-issue",
246
+ report.summary.liveIssueCount,
247
+ report.summary.liveP0IssueCount,
248
+ "Potential runtime breakage in the target OpenClaw/plugin pair. P0 only when it is not a deprecated compat seam.",
249
+ ],
250
+ [
251
+ "compat-gap",
252
+ report.summary.compatGapCount,
253
+ "-",
254
+ "Compatibility behavior is needed but missing from the target OpenClaw compat registry.",
255
+ ],
256
+ [
257
+ "deprecation-warning",
258
+ report.summary.deprecationWarningCount,
259
+ "-",
260
+ "Plugin uses a supported but deprecated compatibility seam; keep it wired while migration exists.",
261
+ ],
262
+ [
263
+ "inspector-gap",
264
+ report.summary.inspectorGapCount,
265
+ "-",
266
+ "Plugin Inspector needs stronger capture/probe evidence before making contract judgments.",
267
+ ],
268
+ [
269
+ "upstream-metadata",
270
+ report.summary.upstreamIssueCount,
271
+ "-",
272
+ "Plugin package or manifest metadata should improve upstream; not a target OpenClaw live break by itself.",
273
+ ],
274
+ [
275
+ "fixture-regression",
276
+ report.summary.fixtureRegressionCount,
277
+ "-",
278
+ "Fixture no longer exposes an expected seam; investigate fixture pin or scanner drift.",
279
+ ],
280
+ ],
281
+ ["Class", "Count", "P0", "Meaning"],
282
+ );
283
+ }
284
+
285
+ function contractProbesTable(probes, options) {
286
+ if (probes.length === 0) {
287
+ return "_none_";
288
+ }
289
+
290
+ return probes.map((probe) => contractProbeBlock(probe, options)).join("\n\n");
291
+ }
292
+
293
+ function contractProbeBlock(probe, options) {
294
+ return [
295
+ `- ${severityLabel(probe.priority, options)} **${probe.fixture}** \`${probe.target}\``,
296
+ ` - contract: ${probe.contract}`,
297
+ ` - id: \`${probe.id}\``,
298
+ " - evidence:",
299
+ ...evidenceList(probe.evidence, options).map((item) => ` - ${item}`),
300
+ ].join("\n");
301
+ }
302
+
303
+ function targetOpenClawTable(targetOpenClaw = {}) {
304
+ const compatRecords = targetOpenClaw.compatRecords ?? [];
305
+ const recordPreview = compatRecords.length > 0 ? compatRecords.join(", ") : "-";
306
+ const statusCounts = Object.values(targetOpenClaw.compatRecordStatuses ?? {}).reduce((counts, status) => {
307
+ counts[status] = (counts[status] ?? 0) + 1;
308
+ return counts;
309
+ }, {});
310
+ return markdownTable(
311
+ [
312
+ ["Configured path", targetOpenClaw.configuredPath ?? "-"],
313
+ ["Status", targetOpenClaw.status],
314
+ ["Compat registry", targetOpenClaw.compatRegistryPath ?? "-"],
315
+ ["Compat records", targetOpenClaw.compatRecordCount ?? 0],
316
+ ["Compat status counts", Object.entries(statusCounts).map(([status, count]) => `${status}:${count}`).join(", ") || "-"],
317
+ ["Record ids", recordPreview],
318
+ ["Hook registry", targetOpenClaw.hookTypesPath ?? "-"],
319
+ ["Hook names", targetOpenClaw.hookNameCount ?? 0],
320
+ ["API builder", targetOpenClaw.apiBuilderPath ?? "-"],
321
+ ["API registrars", targetOpenClaw.apiRegistrarCount ?? 0],
322
+ ["Captured registration", targetOpenClaw.capturedRegistrationPath ?? "-"],
323
+ ["Captured registrars", targetOpenClaw.capturedRegistrarCount ?? 0],
324
+ ["Package metadata", targetOpenClaw.packagePath ?? "-"],
325
+ ["Plugin SDK exports", targetOpenClaw.sdkExportCount ?? 0],
326
+ ["Manifest types", targetOpenClaw.manifestTypesPath ?? "-"],
327
+ ["Manifest fields", targetOpenClaw.manifestFieldCount ?? 0],
328
+ ["Manifest contract fields", targetOpenClaw.manifestContractFieldCount ?? 0],
329
+ ],
330
+ ["Metric", "Value"],
331
+ );
332
+ }
333
+
334
+ function markdownTable(rows, headers) {
335
+ return renderPaddedMarkdownTable(
336
+ rows.map((row) => row.map((cell) => escapeCell(String(cell)))),
337
+ headers.map((header) => escapeCell(String(header))),
338
+ );
339
+ }
340
+
341
+ function escapeCell(value) {
342
+ return String(value).replaceAll("|", "\\|").replaceAll("\n", " ");
343
+ }
344
+
345
+ function severityLabel(severity, options) {
346
+ return { ...defaultSeverityLabels, ...options.severityLabels }[severity] ?? severity ?? "-";
347
+ }
348
+
349
+ function evidenceList(evidence, options) {
350
+ const items = (evidence ?? []).filter(Boolean);
351
+ if (items.length === 0) {
352
+ return ["-"];
353
+ }
354
+ const formatEvidence = options.formatEvidence ?? ((item) => item);
355
+ return items.map((item) => formatEvidence(item));
356
+ }
package/src/config.js ADDED
@@ -0,0 +1,182 @@
1
+ import { existsSync } from "node:fs";
2
+ import { readFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+
5
+ export const npmPackagePayloadDir = ".crabpot-package";
6
+ export const defaultPluginRootConfigFiles = ["plugin-inspector.config.json", ".plugin-inspector.json"];
7
+
8
+ export async function loadInspectorConfig(configPath, options = {}) {
9
+ if (!configPath) {
10
+ throw new Error("--config is required");
11
+ }
12
+ const resolvedPath = path.resolve(options.cwd ?? process.cwd(), configPath);
13
+ const config = JSON.parse(await readFile(resolvedPath, "utf8"));
14
+ const rootDir = path.resolve(options.cwd ?? process.cwd(), options.rootDir ?? path.dirname(resolvedPath));
15
+ const normalizedConfig = await normalizeInspectorConfig(config, { rootDir });
16
+ validateInspectorConfig(normalizedConfig);
17
+ return {
18
+ ...normalizedConfig,
19
+ rootDir,
20
+ configPath: resolvedPath,
21
+ };
22
+ }
23
+
24
+ export async function loadPluginRootConfig(configPath = null, options = {}) {
25
+ const rootDir = path.resolve(options.cwd ?? process.cwd());
26
+ const resolvedPath = configPath ? path.resolve(rootDir, configPath) : findPluginRootConfigPath(rootDir);
27
+ if (!resolvedPath && !existsSync(path.join(rootDir, "package.json")) && !existsSync(path.join(rootDir, "openclaw.plugin.json"))) {
28
+ throw new Error("run from a plugin root with package.json/openclaw.plugin.json, or pass --config");
29
+ }
30
+ const config = resolvedPath ? JSON.parse(await readFile(resolvedPath, "utf8")) : { version: 1 };
31
+ const normalizedConfig = await normalizePluginRootConfig(config, { rootDir });
32
+ validateInspectorConfig(normalizedConfig);
33
+ return {
34
+ ...normalizedConfig,
35
+ rootDir,
36
+ configPath: resolvedPath,
37
+ };
38
+ }
39
+
40
+ export function validateInspectorConfig(config) {
41
+ const errors = [];
42
+
43
+ if (config.version !== 1) {
44
+ errors.push("config.version must be 1");
45
+ }
46
+
47
+ if (!config.submoduleRoot || typeof config.submoduleRoot !== "string") {
48
+ errors.push("config.submoduleRoot must be set");
49
+ }
50
+
51
+ if (!Array.isArray(config.fixtures) || config.fixtures.length === 0) {
52
+ errors.push("config.fixtures must be a non-empty array");
53
+ }
54
+
55
+ const ids = new Set();
56
+ const paths = new Set();
57
+ for (const fixture of config.fixtures ?? []) {
58
+ if (!/^[a-z0-9][a-z0-9-]*$/.test(fixture.id ?? "")) {
59
+ errors.push(`invalid fixture id: ${fixture.id}`);
60
+ }
61
+ if (ids.has(fixture.id)) {
62
+ errors.push(`duplicate fixture id: ${fixture.id}`);
63
+ }
64
+ ids.add(fixture.id);
65
+
66
+ if (typeof fixture.path !== "string" || fixture.path.length === 0) {
67
+ errors.push(`${fixture.id}: path must be set`);
68
+ }
69
+ if (paths.has(fixture.path)) {
70
+ errors.push(`duplicate fixture path: ${fixture.path}`);
71
+ }
72
+ paths.add(fixture.path);
73
+
74
+ const hasRepo = typeof fixture.repo === "string";
75
+ const hasPackage = fixture.package && typeof fixture.package === "object";
76
+ if (hasRepo === hasPackage) {
77
+ errors.push(`${fixture.id}: fixture must declare exactly one of repo or package`);
78
+ }
79
+ if (!["high", "medium", "low"].includes(fixture.priority)) {
80
+ errors.push(`${fixture.id}: priority must be high, medium, or low`);
81
+ }
82
+ if (!Array.isArray(fixture.seams) || fixture.seams.length === 0) {
83
+ errors.push(`${fixture.id}: seams must be non-empty`);
84
+ }
85
+ for (const key of ["hooks", "registrations", "manifestContracts"]) {
86
+ const values = fixture.expect?.[key];
87
+ if (values !== undefined && (!Array.isArray(values) || values.length === 0)) {
88
+ errors.push(`${fixture.id}: expect.${key} must be a non-empty array when present`);
89
+ }
90
+ }
91
+ }
92
+
93
+ if (errors.length > 0) {
94
+ throw new Error(errors.join("\n"));
95
+ }
96
+ }
97
+
98
+ export function fixtureCheckoutPath(config, fixture) {
99
+ return path.resolve(config.rootDir ?? process.cwd(), fixture.path);
100
+ }
101
+
102
+ export function fixtureSourceRoot(config, fixture) {
103
+ const checkoutPath = fixtureCheckoutPath(config, fixture);
104
+ if (fixture.subdir) {
105
+ return path.join(checkoutPath, fixture.subdir);
106
+ }
107
+ if (fixture.package) {
108
+ return path.join(checkoutPath, npmPackagePayloadDir);
109
+ }
110
+ return checkoutPath;
111
+ }
112
+
113
+ export async function normalizePluginRootConfig(config, options = {}) {
114
+ const rootDir = path.resolve(options.rootDir ?? process.cwd());
115
+ const plugin = config.plugin ?? {};
116
+ const packageJson = await readJsonIfExists(path.join(rootDir, "package.json"));
117
+ const pluginManifest = await readJsonIfExists(path.join(rootDir, "openclaw.plugin.json"));
118
+ const sourceRoot = plugin.sourceRoot ?? config.sourceRoot ?? ".";
119
+ const fixture = {
120
+ id: plugin.id ?? pluginManifest?.id ?? packageId(packageJson?.name) ?? "plugin",
121
+ name: plugin.name ?? pluginManifest?.name ?? packageJson?.name ?? "Plugin",
122
+ path: ".",
123
+ repo: "local",
124
+ priority: plugin.priority ?? config.priority ?? "high",
125
+ seams: plugin.seams ?? config.seams ?? inferPluginSeams(pluginManifest, packageJson),
126
+ why: plugin.why ?? config.why ?? "local OpenClaw plugin root",
127
+ expect: plugin.expect ?? config.expect,
128
+ };
129
+
130
+ if (sourceRoot !== ".") {
131
+ fixture.subdir = sourceRoot;
132
+ }
133
+
134
+ return {
135
+ version: 1,
136
+ submoduleRoot: ".",
137
+ openclaw: config.openclaw,
138
+ fixtures: [fixture],
139
+ };
140
+ }
141
+
142
+ export async function normalizeInspectorConfig(config, options = {}) {
143
+ if (Array.isArray(config.fixtures)) {
144
+ return config;
145
+ }
146
+ return normalizePluginRootConfig(config, options);
147
+ }
148
+
149
+ function findPluginRootConfigPath(rootDir) {
150
+ return defaultPluginRootConfigFiles.map((file) => path.join(rootDir, file)).find(existsSync) ?? null;
151
+ }
152
+
153
+ async function readJsonIfExists(filePath) {
154
+ if (!existsSync(filePath)) {
155
+ return null;
156
+ }
157
+ return JSON.parse(await readFile(filePath, "utf8"));
158
+ }
159
+
160
+ function packageId(packageName) {
161
+ if (!packageName) {
162
+ return null;
163
+ }
164
+ return packageName
165
+ .split("/")
166
+ .pop()
167
+ .replace(/^openclaw-/, "")
168
+ .replace(/[^a-zA-Z0-9]+/g, "-")
169
+ .replace(/^-+|-+$/g, "")
170
+ .toLowerCase();
171
+ }
172
+
173
+ function inferPluginSeams(pluginManifest, packageJson) {
174
+ const contracts = Object.keys(pluginManifest?.contracts ?? {});
175
+ if (contracts.includes("tools")) {
176
+ return ["dynamic-tool"];
177
+ }
178
+ if (packageJson?.openclaw?.extensions || packageJson?.openclaw?.runtimeExtensions) {
179
+ return ["plugin-runtime"];
180
+ }
181
+ return ["plugin-metadata"];
182
+ }