@openclaw/plugin-inspector 0.3.10 → 0.3.12
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.
- package/CHANGELOG.md +21 -0
- package/README.md +6 -2
- package/package.json +8 -2
- package/src/advanced.js +1 -0
- package/src/api.js +4 -0
- package/src/batch.js +297 -0
- package/src/capture-cli.js +12 -1
- package/src/capture-config.js +49 -1
- package/src/cli.js +103 -6
- package/src/compatibility-report.js +4 -4
- package/src/contract-coverage.js +6 -1
- package/src/contract-probes.js +5 -0
- package/src/execution-results.js +10 -0
- package/src/fixture-summary.js +101 -4
- package/src/index.js +15 -0
- package/src/inspector.js +1 -0
- package/src/issues.js +17 -2
- package/src/report.js +26 -9
- package/src/runtime-reconciliation.js +4 -1
- package/src/sdk-mock.js +153 -1
- package/src/synthetic-entrypoint.js +47 -0
- package/src/synthetic-probes-cli.js +2 -35
- package/src/synthetic-probes.js +59 -0
package/src/cli.js
CHANGED
|
@@ -3,6 +3,7 @@ import path from "node:path";
|
|
|
3
3
|
import {
|
|
4
4
|
loadPluginConfig,
|
|
5
5
|
renderTextSummary,
|
|
6
|
+
runBatchAnalysis,
|
|
6
7
|
sanitizeReportArtifact,
|
|
7
8
|
runPluginCheck,
|
|
8
9
|
} from "./index.js";
|
|
@@ -43,6 +44,8 @@ try {
|
|
|
43
44
|
}
|
|
44
45
|
} else if (command === "ci") {
|
|
45
46
|
await runCi(commandArgs);
|
|
47
|
+
} else if (command === "batch") {
|
|
48
|
+
await runBatch(commandArgs);
|
|
46
49
|
} else if (command === "capture") {
|
|
47
50
|
await runCapture(commandArgs);
|
|
48
51
|
} else {
|
|
@@ -53,6 +56,38 @@ try {
|
|
|
53
56
|
process.exitCode = 1;
|
|
54
57
|
}
|
|
55
58
|
|
|
59
|
+
async function runBatch(commandArgs) {
|
|
60
|
+
const inputDir = readFirstPositional(commandArgs, new Set(["--out", "--openclaw", "--concurrency"]));
|
|
61
|
+
const outDir = readFlag(commandArgs, "--out") ?? "reports";
|
|
62
|
+
const openclawPath = commandArgs.includes("--no-openclaw") ? false : readFlag(commandArgs, "--openclaw");
|
|
63
|
+
const concurrency = Number(readFlag(commandArgs, "--concurrency") ?? "4");
|
|
64
|
+
const json = commandArgs.includes("--json");
|
|
65
|
+
const check = commandArgs.includes("--check");
|
|
66
|
+
const keepPluginReports = commandArgs.includes("--keep-plugin-reports");
|
|
67
|
+
const includeInspectorGaps = commandArgs.includes("--include-inspector-gaps");
|
|
68
|
+
if (!inputDir) {
|
|
69
|
+
throw new Error("batch requires a folder of plugin roots");
|
|
70
|
+
}
|
|
71
|
+
const { report, paths } = await runBatchAnalysis({
|
|
72
|
+
rootDir: inputDir,
|
|
73
|
+
outDir,
|
|
74
|
+
openclawPath,
|
|
75
|
+
concurrency,
|
|
76
|
+
includeInspectorGaps,
|
|
77
|
+
keepPluginReports,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
if (json) {
|
|
81
|
+
console.log(JSON.stringify(report, null, 2));
|
|
82
|
+
} else {
|
|
83
|
+
console.log(renderBatchTextSummary(report, paths));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (check && report.summary.pluginsWithErrors > 0) {
|
|
87
|
+
throw new Error(`plugin-inspector batch found ${report.summary.pluginsWithErrors} plugin(s) with errors`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
56
91
|
async function runConfig(commandArgs) {
|
|
57
92
|
const configPath = readFlag(commandArgs, "--config");
|
|
58
93
|
const pluginRoot = readFlag(commandArgs, "--plugin-root") ?? readFlag(commandArgs, "--root");
|
|
@@ -75,10 +110,12 @@ async function runCheck(commandArgs) {
|
|
|
75
110
|
const mockSdk = readMockSdkFlag(commandArgs);
|
|
76
111
|
const allowExecution = readAllowExecutionFlag(commandArgs);
|
|
77
112
|
const ciOutputs = readCiOutputFlags(commandArgs);
|
|
113
|
+
const includeInspectorGaps = commandArgs.includes("--include-inspector-gaps");
|
|
78
114
|
const { report, paths } = await runPluginCheck({
|
|
79
115
|
allowExecution,
|
|
80
116
|
capture,
|
|
81
117
|
configPath,
|
|
118
|
+
includeInspectorGaps,
|
|
82
119
|
mockSdk,
|
|
83
120
|
openclawPath,
|
|
84
121
|
outDir,
|
|
@@ -164,10 +201,12 @@ async function runCi(commandArgs) {
|
|
|
164
201
|
const mockSdk = readMockSdkFlag(commandArgs);
|
|
165
202
|
const allowExecution = readAllowExecutionFlag(commandArgs);
|
|
166
203
|
const ciOutputs = readCiOutputFlags(commandArgs, { defaultEnabled: true });
|
|
204
|
+
const includeInspectorGaps = commandArgs.includes("--include-inspector-gaps");
|
|
167
205
|
const { report, reportDir } = await runCiCompatibilityReport({
|
|
168
206
|
allowExecution,
|
|
169
207
|
capture,
|
|
170
208
|
configPath,
|
|
209
|
+
includeInspectorGaps,
|
|
171
210
|
mockSdk,
|
|
172
211
|
openclawPath,
|
|
173
212
|
outDir,
|
|
@@ -204,10 +243,19 @@ async function runCi(commandArgs) {
|
|
|
204
243
|
}
|
|
205
244
|
}
|
|
206
245
|
|
|
207
|
-
async function runCiCompatibilityReport({
|
|
246
|
+
async function runCiCompatibilityReport({
|
|
247
|
+
allowExecution,
|
|
248
|
+
capture,
|
|
249
|
+
configPath,
|
|
250
|
+
includeInspectorGaps,
|
|
251
|
+
mockSdk,
|
|
252
|
+
openclawPath,
|
|
253
|
+
outDir,
|
|
254
|
+
pluginRoot,
|
|
255
|
+
}) {
|
|
208
256
|
if (configPath) {
|
|
209
257
|
const config = await loadInspectorConfig(configPath, { cwd: pluginRoot });
|
|
210
|
-
const report = await inspectCompatibilityFixtureSet(config, { openclawPath });
|
|
258
|
+
const report = await inspectCompatibilityFixtureSet(config, { includeInspectorGaps, openclawPath });
|
|
211
259
|
await writeCompatibilityReport(report, { cwd: config.rootDir, outDir });
|
|
212
260
|
return {
|
|
213
261
|
report,
|
|
@@ -215,7 +263,15 @@ async function runCiCompatibilityReport({ allowExecution, capture, configPath, m
|
|
|
215
263
|
};
|
|
216
264
|
}
|
|
217
265
|
|
|
218
|
-
const { report } = await runPluginCheck({
|
|
266
|
+
const { report } = await runPluginCheck({
|
|
267
|
+
allowExecution,
|
|
268
|
+
capture,
|
|
269
|
+
includeInspectorGaps,
|
|
270
|
+
mockSdk,
|
|
271
|
+
openclawPath,
|
|
272
|
+
outDir,
|
|
273
|
+
pluginRoot,
|
|
274
|
+
});
|
|
219
275
|
return {
|
|
220
276
|
report,
|
|
221
277
|
reportDir: path.resolve(pluginRoot ?? process.cwd(), outDir),
|
|
@@ -223,7 +279,7 @@ async function runCiCompatibilityReport({ allowExecution, capture, configPath, m
|
|
|
223
279
|
}
|
|
224
280
|
|
|
225
281
|
async function runCapture(commandArgs) {
|
|
226
|
-
const entrypoint = commandArgs
|
|
282
|
+
const entrypoint = findCaptureEntrypoint(commandArgs);
|
|
227
283
|
const outputPath = readFlag(commandArgs, "--output");
|
|
228
284
|
const pluginRoot = readFlag(commandArgs, "--plugin-root");
|
|
229
285
|
const mockSdk = readMockSdkFlag(commandArgs) ?? commandArgs.includes("--mock-sdk");
|
|
@@ -252,6 +308,17 @@ function readFlag(commandArgs, name) {
|
|
|
252
308
|
return commandArgs[index + 1] ?? null;
|
|
253
309
|
}
|
|
254
310
|
|
|
311
|
+
function findCaptureEntrypoint(commandArgs) {
|
|
312
|
+
const flagsWithValues = new Set(["--output", "--plugin-root", "--sdk"]);
|
|
313
|
+
const consumedIndexes = new Set();
|
|
314
|
+
for (const [index, arg] of commandArgs.entries()) {
|
|
315
|
+
if (flagsWithValues.has(arg)) {
|
|
316
|
+
consumedIndexes.add(index + 1);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return commandArgs.find((arg, index) => !arg.startsWith("-") && !consumedIndexes.has(index)) ?? null;
|
|
320
|
+
}
|
|
321
|
+
|
|
255
322
|
function readOptionalPathFlag(commandArgs, name, defaultPath) {
|
|
256
323
|
const index = commandArgs.indexOf(name);
|
|
257
324
|
if (index === -1) {
|
|
@@ -315,6 +382,34 @@ function renderCiTextSummary(summary) {
|
|
|
315
382
|
].join("\n");
|
|
316
383
|
}
|
|
317
384
|
|
|
385
|
+
function renderBatchTextSummary(report, paths) {
|
|
386
|
+
const lines = [
|
|
387
|
+
"Plugin Inspector Batch",
|
|
388
|
+
`Plugins: ${report.summary.pluginCount}`,
|
|
389
|
+
`Plugins with errors: ${report.summary.pluginsWithErrors}`,
|
|
390
|
+
`Plugins with warnings: ${report.summary.pluginsWithWarnings}`,
|
|
391
|
+
`Finding codes: ${report.summary.findingCodeCount}`,
|
|
392
|
+
"",
|
|
393
|
+
"Reports:",
|
|
394
|
+
`- JSON: ${paths.jsonPath}`,
|
|
395
|
+
`- Markdown: ${paths.markdownPath}`,
|
|
396
|
+
];
|
|
397
|
+
const topFinding = report.findingFrequency[0];
|
|
398
|
+
if (topFinding) {
|
|
399
|
+
lines.push("", `Top finding: ${topFinding.code} (${topFinding.plugins} plugin(s))`);
|
|
400
|
+
}
|
|
401
|
+
return lines.join("\n");
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function readFirstPositional(args, valueFlags = new Set()) {
|
|
405
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
406
|
+
const arg = args[index];
|
|
407
|
+
if (!arg.startsWith("-")) return arg;
|
|
408
|
+
if (valueFlags.has(arg)) index += 1;
|
|
409
|
+
}
|
|
410
|
+
return undefined;
|
|
411
|
+
}
|
|
412
|
+
|
|
318
413
|
function initCommandSummary(result) {
|
|
319
414
|
return {
|
|
320
415
|
dryRun: result.dryRun,
|
|
@@ -342,16 +437,18 @@ function printHelp() {
|
|
|
342
437
|
|
|
343
438
|
Usage:
|
|
344
439
|
plugin-inspector
|
|
345
|
-
plugin-inspector check [--plugin-root <path>] [--config <path>] [--out <dir>] [--openclaw <path>] [--no-openclaw] [--runtime] [--mock-sdk|--real-sdk] [--allow-execute] [--json]
|
|
440
|
+
plugin-inspector check [--plugin-root <path>] [--config <path>] [--out <dir>] [--openclaw <path>] [--no-openclaw] [--runtime] [--mock-sdk|--real-sdk] [--allow-execute] [--include-inspector-gaps] [--json]
|
|
346
441
|
plugin-inspector config [--plugin-root <path>] [--config <path>] [--json]
|
|
347
442
|
plugin-inspector init [--plugin-root <path>] [--config <path>] [--ci] [--scripts] [--package-manager npm|pnpm|yarn|bun] [--dry-run] [--json] [--force]
|
|
348
443
|
plugin-inspector report --config <path> [--out <dir>] [--check] [--json]
|
|
444
|
+
plugin-inspector batch <folder> [--out <dir>] [--openclaw <path>] [--no-openclaw] [--concurrency <n>] [--keep-plugin-reports] [--include-inspector-gaps] [--check] [--json]
|
|
349
445
|
plugin-inspector inspect [--plugin-root <path>] [--config <path>] [--out <dir>] [--check] [--json] [--sarif [path]] [--junit [path]] [--allow-execute]
|
|
350
|
-
plugin-inspector ci [--plugin-root <path>] [--config <path>] [--out <dir>] [--openclaw <path>] [--no-openclaw] [--runtime] [--mock-sdk|--real-sdk] [--allow-execute] [--json] [--no-sarif] [--no-junit]
|
|
446
|
+
plugin-inspector ci [--plugin-root <path>] [--config <path>] [--out <dir>] [--openclaw <path>] [--no-openclaw] [--runtime] [--mock-sdk|--real-sdk] [--allow-execute] [--include-inspector-gaps] [--json] [--no-sarif] [--no-junit]
|
|
351
447
|
plugin-inspector capture <entrypoint> [--mock-sdk|--real-sdk] [--allow-execute] [--plugin-root <path>] [--output <path>]
|
|
352
448
|
|
|
353
449
|
Default check runs from the current plugin root and writes reports/ unless --out is set.
|
|
354
450
|
CI writes SARIF and JUnit artifacts by default; check/inspect can write them with --sarif and --junit.
|
|
355
451
|
Runtime capture is opt-in because it imports plugin code; use --runtime with --allow-execute or PLUGIN_INSPECTOR_EXECUTE_ISOLATED=1.
|
|
452
|
+
Inspector coverage gaps are hidden by default; pass --include-inspector-gaps for maintainer coverage reports.
|
|
356
453
|
`);
|
|
357
454
|
}
|
|
@@ -58,9 +58,9 @@ export function renderCompatibilityMarkdownReport(report, options = {}) {
|
|
|
58
58
|
options,
|
|
59
59
|
),
|
|
60
60
|
"",
|
|
61
|
-
"## Live Issues",
|
|
61
|
+
"## Other Live Issues",
|
|
62
62
|
"",
|
|
63
|
-
issuesTable(report.issues.filter((issue) => issue.issueClass === "live-issue"), options),
|
|
63
|
+
issuesTable(report.issues.filter((issue) => issue.issueClass === "live-issue" && issue.severity !== "P0"), options),
|
|
64
64
|
"",
|
|
65
65
|
"## Compat Gaps",
|
|
66
66
|
"",
|
|
@@ -183,9 +183,9 @@ export function renderCompatibilityIssuesReport(report, options = {}) {
|
|
|
183
183
|
options,
|
|
184
184
|
),
|
|
185
185
|
"",
|
|
186
|
-
"## Live Issues",
|
|
186
|
+
"## Other Live Issues",
|
|
187
187
|
"",
|
|
188
|
-
issuesTable(report.issues.filter((issue) => issue.issueClass === "live-issue"), options),
|
|
188
|
+
issuesTable(report.issues.filter((issue) => issue.issueClass === "live-issue" && issue.severity !== "P0"), options),
|
|
189
189
|
"",
|
|
190
190
|
"## Compat Gaps",
|
|
191
191
|
"",
|
package/src/contract-coverage.js
CHANGED
|
@@ -157,10 +157,15 @@ function requireCompatRecordReconciliation(report, errors) {
|
|
|
157
157
|
.filter((finding) => finding.code === "missing-compat-record")
|
|
158
158
|
.map((finding) => `${finding.fixture}:${finding.compatRecord}`),
|
|
159
159
|
);
|
|
160
|
+
const compatGapRecords = new Set(
|
|
161
|
+
report.issues
|
|
162
|
+
.filter((issue) => issue.issueClass === "compat-gap" && issue.compatRecord)
|
|
163
|
+
.map((issue) => `${issue.fixture}:${issue.compatRecord}`),
|
|
164
|
+
);
|
|
160
165
|
|
|
161
166
|
for (const finding of [...report.warnings, ...report.suggestions].filter((item) => item.compatRecord)) {
|
|
162
167
|
const key = `${finding.fixture}:${finding.compatRecord}`;
|
|
163
|
-
if (!presentRecords.has(key) && !missingRecords.has(key)) {
|
|
168
|
+
if (!presentRecords.has(key) && !missingRecords.has(key) && !compatGapRecords.has(key)) {
|
|
164
169
|
errors.push(`${finding.fixture}: compat record ${finding.compatRecord} was not reconciled`);
|
|
165
170
|
}
|
|
166
171
|
}
|
package/src/contract-probes.js
CHANGED
|
@@ -114,6 +114,11 @@ export const contractProbeRules = {
|
|
|
114
114
|
contract: "Package and OpenClaw manifest versions stay aligned for release compatibility reporting.",
|
|
115
115
|
target: "package-loader",
|
|
116
116
|
},
|
|
117
|
+
"manifest-name-missing": {
|
|
118
|
+
id: "manifest.metadata.name",
|
|
119
|
+
contract: "OpenClaw plugin manifests declare a human-readable display name for registry and tooling metadata.",
|
|
120
|
+
target: "manifest-loader",
|
|
121
|
+
},
|
|
117
122
|
"package-plugin-api-compat-missing": {
|
|
118
123
|
id: "package.compat.plugin-api-range",
|
|
119
124
|
contract: "Package metadata declares the OpenClaw plugin API range used by the plugin.",
|
package/src/execution-results.js
CHANGED
|
@@ -204,6 +204,7 @@ function summarizeArtifact({ artifactPath, parsed, rootDir }) {
|
|
|
204
204
|
: "capture";
|
|
205
205
|
const fixture = normalizedArtifactPath.split("/").at(-2) ?? "unknown";
|
|
206
206
|
if (kind === "synthetic") {
|
|
207
|
+
const passed = (parsed.results ?? []).filter((result) => result.status === "pass");
|
|
207
208
|
return {
|
|
208
209
|
artifactPath: normalizedArtifactPath,
|
|
209
210
|
fixture,
|
|
@@ -211,6 +212,8 @@ function summarizeArtifact({ artifactPath, parsed, rootDir }) {
|
|
|
211
212
|
entrypoint: scrubPath(parsed.entrypoint, { rootDir }),
|
|
212
213
|
status: parsed.status,
|
|
213
214
|
summary: parsed.summary,
|
|
215
|
+
captured: passed.map(syntheticRuntimeCaptureKey).filter(Boolean),
|
|
216
|
+
passed,
|
|
214
217
|
failures: (parsed.results ?? []).filter((result) => result.status === "fail"),
|
|
215
218
|
blocked: (parsed.results ?? []).filter((result) => result.status === "blocked"),
|
|
216
219
|
};
|
|
@@ -261,6 +264,13 @@ function summarizeArtifactResult(artifact) {
|
|
|
261
264
|
return `${artifact.capturedCount} captured`;
|
|
262
265
|
}
|
|
263
266
|
|
|
267
|
+
function syntheticRuntimeCaptureKey(result) {
|
|
268
|
+
if (!result.kind || !result.seam) {
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
271
|
+
return `${result.kind}:${result.seam}`;
|
|
272
|
+
}
|
|
273
|
+
|
|
264
274
|
function auditFindingCount(parsed) {
|
|
265
275
|
const vulnerabilities = parsed.metadata?.vulnerabilities;
|
|
266
276
|
if (vulnerabilities && typeof vulnerabilities === "object") {
|
package/src/fixture-summary.js
CHANGED
|
@@ -233,6 +233,26 @@ export function classifyPackageContracts({ fixture, inspection, fixtureReport })
|
|
|
233
233
|
});
|
|
234
234
|
}
|
|
235
235
|
|
|
236
|
+
const missingManifestNames = fixtureReport.pluginManifests.filter(
|
|
237
|
+
(manifest) => typeof manifest.name !== "string" || manifest.name.trim().length === 0,
|
|
238
|
+
);
|
|
239
|
+
if (missingManifestNames.length > 0) {
|
|
240
|
+
warnings.push({
|
|
241
|
+
fixture: fixture.id,
|
|
242
|
+
code: "manifest-name-missing",
|
|
243
|
+
level: "warning",
|
|
244
|
+
message: "openclaw.plugin.json does not declare a display name",
|
|
245
|
+
evidence: missingManifestNames.map((manifest) => manifest.path ?? "openclaw.plugin.json"),
|
|
246
|
+
});
|
|
247
|
+
decisions.push({
|
|
248
|
+
fixture: fixture.id,
|
|
249
|
+
decision: "plugin-upstream-fix",
|
|
250
|
+
seam: "manifest-metadata",
|
|
251
|
+
action: "Ask the plugin to declare openclaw.plugin.json name so registries and tools can derive a human-readable title.",
|
|
252
|
+
evidence: missingManifestNames.map((manifest) => manifest.path ?? "openclaw.plugin.json").join(", "),
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
|
|
236
256
|
if (packageSummary.openclaw && !packageSummary.openclaw.compatPluginApi) {
|
|
237
257
|
warnings.push({
|
|
238
258
|
fixture: fixture.id,
|
|
@@ -340,9 +360,12 @@ export function classifyPackageContracts({ fixture, inspection, fixtureReport })
|
|
|
340
360
|
});
|
|
341
361
|
}
|
|
342
362
|
|
|
343
|
-
const
|
|
363
|
+
const entrypoints = packageSummary.openclaw?.entrypoints ?? [];
|
|
364
|
+
const missingEntrypoints = entrypoints.filter((entrypoint) => !entrypoint.exists);
|
|
344
365
|
const buildEntrypoints = missingEntrypoints.filter((entrypoint) => entrypoint.requiresBuild);
|
|
345
|
-
const plainMissingEntrypoints = missingEntrypoints.filter(
|
|
366
|
+
const plainMissingEntrypoints = missingEntrypoints.filter(
|
|
367
|
+
(entrypoint) => !entrypoint.requiresBuild && !hasUsablePackageRuntimeEntrypoint(entrypoint, packageSummary, entrypoints),
|
|
368
|
+
);
|
|
346
369
|
|
|
347
370
|
if (buildEntrypoints.length > 0) {
|
|
348
371
|
suggestions.push({
|
|
@@ -920,6 +943,46 @@ function collectOpenClawEntrypoints(packageDir, openclaw, options) {
|
|
|
920
943
|
});
|
|
921
944
|
}
|
|
922
945
|
|
|
946
|
+
function hasUsablePackageRuntimeEntrypoint(entrypoint, packageSummary, entrypoints) {
|
|
947
|
+
if (!isSourceEntrypoint(entrypoint.specifier)) {
|
|
948
|
+
return false;
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
const runtimeBuildSpecifier = runtimeBuildSpecifierFor(entrypoint.specifier);
|
|
952
|
+
if (
|
|
953
|
+
entrypoints.some(
|
|
954
|
+
(candidate) =>
|
|
955
|
+
candidate.exists &&
|
|
956
|
+
candidate.requiresBuild &&
|
|
957
|
+
normalizeEntrypointSpecifier(candidate.specifier) === normalizeEntrypointSpecifier(runtimeBuildSpecifier),
|
|
958
|
+
)
|
|
959
|
+
) {
|
|
960
|
+
return true;
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
if (entrypoint.kind === "extension" && entrypoints.some((candidate) => candidate.kind === "runtimeExtension" && candidate.exists)) {
|
|
964
|
+
return true;
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
const packageDir = path.dirname(packageSummary.path);
|
|
968
|
+
return existsSync(path.resolve(packageDir, runtimeBuildSpecifier));
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
function isSourceEntrypoint(specifier) {
|
|
972
|
+
return /\.(?:ts|tsx)$/.test(specifier);
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
function runtimeBuildSpecifierFor(specifier) {
|
|
976
|
+
const normalized = normalizeEntrypointSpecifier(specifier);
|
|
977
|
+
const basename = path.posix.basename(normalized).replace(/\.(?:ts|tsx)$/, ".js");
|
|
978
|
+
return `./dist/${basename}`;
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
function normalizeEntrypointSpecifier(specifier) {
|
|
982
|
+
const normalized = specifier.replaceAll("\\", "/");
|
|
983
|
+
return normalized.startsWith("./") ? normalized : `./${normalized}`;
|
|
984
|
+
}
|
|
985
|
+
|
|
923
986
|
async function findPackageFiles(root, options, depth = 0) {
|
|
924
987
|
if (!existsSync(root) || depth > options.maxDepth) {
|
|
925
988
|
return [];
|
|
@@ -1059,8 +1122,13 @@ function packageNpmPackIssues(packageSummary, fixtureReport) {
|
|
|
1059
1122
|
});
|
|
1060
1123
|
}
|
|
1061
1124
|
|
|
1062
|
-
const
|
|
1063
|
-
|
|
1125
|
+
const entrypoints = packageSummary.openclaw?.entrypoints ?? [];
|
|
1126
|
+
const missingEntrypoints = entrypoints
|
|
1127
|
+
.filter(
|
|
1128
|
+
(entrypoint) =>
|
|
1129
|
+
!repoPathIncludedInNpmPack(packageSummary, entrypoint.relativePath) &&
|
|
1130
|
+
!hasPackagedRuntimeEntrypoint(entrypoint, packageSummary, entrypoints),
|
|
1131
|
+
)
|
|
1064
1132
|
.map((entrypoint) => `${entrypoint.kind}:${entrypoint.specifier} -> ${entrypoint.relativePath}`) ?? [];
|
|
1065
1133
|
if (missingEntrypoints.length > 0) {
|
|
1066
1134
|
findings.push({
|
|
@@ -1088,6 +1156,35 @@ function packageNpmPackMissingMetadata(packageSummary, fixtureReport) {
|
|
|
1088
1156
|
return missing;
|
|
1089
1157
|
}
|
|
1090
1158
|
|
|
1159
|
+
function hasPackagedRuntimeEntrypoint(entrypoint, packageSummary, entrypoints) {
|
|
1160
|
+
if (!isSourceEntrypoint(entrypoint.specifier)) {
|
|
1161
|
+
return false;
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
const runtimeBuildSpecifier = runtimeBuildSpecifierFor(entrypoint.specifier);
|
|
1165
|
+
const matchingRuntimeEntrypoint = entrypoints.find(
|
|
1166
|
+
(candidate) =>
|
|
1167
|
+
candidate.requiresBuild &&
|
|
1168
|
+
normalizeEntrypointSpecifier(candidate.specifier) === normalizeEntrypointSpecifier(runtimeBuildSpecifier),
|
|
1169
|
+
);
|
|
1170
|
+
if (matchingRuntimeEntrypoint && repoPathIncludedInNpmPack(packageSummary, matchingRuntimeEntrypoint.relativePath)) {
|
|
1171
|
+
return true;
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
if (
|
|
1175
|
+
entrypoint.kind === "extension" &&
|
|
1176
|
+
entrypoints.some(
|
|
1177
|
+
(candidate) => candidate.kind === "runtimeExtension" && repoPathIncludedInNpmPack(packageSummary, candidate.relativePath),
|
|
1178
|
+
)
|
|
1179
|
+
) {
|
|
1180
|
+
return true;
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
const packageDir = path.posix.dirname(normalizeRepoPath(packageSummary.path));
|
|
1184
|
+
const runtimeBuildPath = path.posix.join(packageDir === "." ? "" : packageDir, normalizeEntrypointSpecifier(runtimeBuildSpecifier));
|
|
1185
|
+
return repoPathIncludedInNpmPack(packageSummary, runtimeBuildPath);
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1091
1188
|
function packageMinHostVersionDrift(packageSummary) {
|
|
1092
1189
|
const openclaw = packageSummary.openclaw;
|
|
1093
1190
|
if (!nonEmptyString(openclaw?.install?.minHostVersion) || !nonEmptyString(openclaw?.buildOpenClawVersion)) {
|
package/src/index.js
CHANGED
|
@@ -14,8 +14,10 @@ import * as refDiffApi from "./ref-diff.js";
|
|
|
14
14
|
import * as reportApi from "./report.js";
|
|
15
15
|
import * as runtimeProfileApi from "./runtime-profile.js";
|
|
16
16
|
import * as runtimeReconciliationApi from "./runtime-reconciliation.js";
|
|
17
|
+
import * as syntheticEntrypointApi from "./synthetic-entrypoint.js";
|
|
17
18
|
import * as syntheticProbeSuiteApi from "./synthetic-probe-suite.js";
|
|
18
19
|
import * as syntheticProbesApi from "./synthetic-probes.js";
|
|
20
|
+
import * as batchApi from "./batch.js";
|
|
19
21
|
|
|
20
22
|
export const pluginRoot = Object.freeze({
|
|
21
23
|
loadConfig: pluginApi.loadPluginConfig,
|
|
@@ -25,6 +27,12 @@ export const pluginRoot = Object.freeze({
|
|
|
25
27
|
setup: pluginApi.setupPluginInspector,
|
|
26
28
|
});
|
|
27
29
|
|
|
30
|
+
export const batch = Object.freeze({
|
|
31
|
+
discoverPluginRoots: batchApi.discoverPluginRoots,
|
|
32
|
+
run: batchApi.runBatchAnalysis,
|
|
33
|
+
writeReport: batchApi.writeBatchReport,
|
|
34
|
+
});
|
|
35
|
+
|
|
28
36
|
export const fixtureSuites = Object.freeze({
|
|
29
37
|
loadConfig: configApi.loadInspectorConfig,
|
|
30
38
|
inspect: pluginApi.inspectCompatibilityFixtureSetConfig,
|
|
@@ -123,12 +131,18 @@ export const synthetic = Object.freeze({
|
|
|
123
131
|
renderPlan: syntheticProbesApi.renderSyntheticProbeMarkdown,
|
|
124
132
|
validatePlan: syntheticProbesApi.validateSyntheticProbePlan,
|
|
125
133
|
runCaptured: syntheticProbesApi.runCapturedSyntheticProbes,
|
|
134
|
+
runEntrypoint: syntheticEntrypointApi.runEntrypointSyntheticProbes,
|
|
126
135
|
registrationExecutionProfiles: syntheticProbesApi.syntheticRegistrationExecutionProfiles,
|
|
127
136
|
defaultHookEvents: syntheticProbesApi.defaultSyntheticHookEvents,
|
|
128
137
|
defaultHookContexts: syntheticProbesApi.defaultSyntheticHookContexts,
|
|
129
138
|
defaultRegistrationArguments: syntheticProbesApi.defaultSyntheticRegistrationArguments,
|
|
130
139
|
});
|
|
131
140
|
|
|
141
|
+
export {
|
|
142
|
+
discoverPluginRoots,
|
|
143
|
+
runBatchAnalysis,
|
|
144
|
+
writeBatchReport,
|
|
145
|
+
} from "./batch.js";
|
|
132
146
|
export {
|
|
133
147
|
capturePluginEntrypoint,
|
|
134
148
|
buildFixtureSetColdImportReadiness,
|
|
@@ -234,6 +248,7 @@ export {
|
|
|
234
248
|
applyRuntimeExecutionCoverage,
|
|
235
249
|
buildRuntimeExecutionCoverage,
|
|
236
250
|
} from "./runtime-reconciliation.js";
|
|
251
|
+
export { runEntrypointSyntheticProbes } from "./synthetic-entrypoint.js";
|
|
237
252
|
export { buildSyntheticProbePlanFromReport } from "./synthetic-probe-suite.js";
|
|
238
253
|
export {
|
|
239
254
|
buildSyntheticProbePlan,
|
package/src/inspector.js
CHANGED
|
@@ -37,6 +37,7 @@ export async function inspectCompatibilityFixtureSet(config, options = {}) {
|
|
|
37
37
|
failures,
|
|
38
38
|
generatedAt: options.generatedAt,
|
|
39
39
|
executionResults: options.executionResults,
|
|
40
|
+
includeInspectorGaps: options.includeInspectorGaps,
|
|
40
41
|
targetOpenClaw,
|
|
41
42
|
buildFixtureReport: ({ fixture, inspection }) =>
|
|
42
43
|
buildCompatibilityFixtureReport({
|
package/src/issues.js
CHANGED
|
@@ -14,6 +14,7 @@ export const knownIssueCodes = new Set([
|
|
|
14
14
|
"conversation-access-hook",
|
|
15
15
|
"legacy-before-agent-start",
|
|
16
16
|
"legacy-root-sdk-import",
|
|
17
|
+
"manifest-name-missing",
|
|
17
18
|
"manifest-unknown-contracts",
|
|
18
19
|
"manifest-unknown-fields",
|
|
19
20
|
"missing-expected-seam",
|
|
@@ -111,6 +112,12 @@ export const issueMetadataByCode = {
|
|
|
111
112
|
decision: "inspector-follow-up",
|
|
112
113
|
title: "fixture no longer exposes an expected seam",
|
|
113
114
|
},
|
|
115
|
+
"manifest-name-missing": {
|
|
116
|
+
severity: "P2",
|
|
117
|
+
owner: "plugin",
|
|
118
|
+
decision: "plugin-upstream-fix",
|
|
119
|
+
title: "manifest display name is missing",
|
|
120
|
+
},
|
|
114
121
|
"manifest-unknown-contracts": {
|
|
115
122
|
severity: "P1",
|
|
116
123
|
owner: "plugin",
|
|
@@ -326,6 +333,10 @@ export function classifyIssueFinding(finding, targetOpenClaw, metadata = {}) {
|
|
|
326
333
|
};
|
|
327
334
|
}
|
|
328
335
|
|
|
336
|
+
export function isInspectorGapFinding(finding, targetOpenClaw) {
|
|
337
|
+
return issueMetadata(finding, targetOpenClaw).issueClass === "inspector-gap";
|
|
338
|
+
}
|
|
339
|
+
|
|
329
340
|
export function summarizeIssueClasses(issues) {
|
|
330
341
|
const summary = {
|
|
331
342
|
"compat-gap": 0,
|
|
@@ -342,7 +353,10 @@ export function summarizeIssueClasses(issues) {
|
|
|
342
353
|
}
|
|
343
354
|
|
|
344
355
|
function issueClassFor(code, options) {
|
|
345
|
-
if (
|
|
356
|
+
if (code === "sdk-export-missing" && options.compatRecord) {
|
|
357
|
+
return "compat-gap";
|
|
358
|
+
}
|
|
359
|
+
if (["unknown-hook-name", "unknown-registration-name", "package-entrypoint-missing"].includes(code)) {
|
|
346
360
|
return "live-issue";
|
|
347
361
|
}
|
|
348
362
|
if (code === "missing-compat-record") {
|
|
@@ -369,6 +383,7 @@ function issueClassFor(code, options) {
|
|
|
369
383
|
[
|
|
370
384
|
"manifest-unknown-contracts",
|
|
371
385
|
"manifest-unknown-fields",
|
|
386
|
+
"manifest-name-missing",
|
|
372
387
|
"package-json-missing",
|
|
373
388
|
"package-manifest-version-drift",
|
|
374
389
|
"package-min-host-version-drift",
|
|
@@ -397,7 +412,7 @@ function severityForClass(code, defaultSeverity, options) {
|
|
|
397
412
|
if (
|
|
398
413
|
options.issueClass === "live-issue" &&
|
|
399
414
|
["none", "untracked"].includes(options.compatStatus) &&
|
|
400
|
-
["unknown-hook-name", "unknown-registration-name", "package-entrypoint-missing"
|
|
415
|
+
["unknown-hook-name", "unknown-registration-name", "package-entrypoint-missing"].includes(code)
|
|
401
416
|
) {
|
|
402
417
|
return "P0";
|
|
403
418
|
}
|
package/src/report.js
CHANGED
|
@@ -3,7 +3,7 @@ import { renderMarkdownTable, writeArtifacts, writeJsonMarkdownArtifacts } from
|
|
|
3
3
|
import { renderCompatibilityIssuesReport, renderCompatibilityMarkdownReport } from "./compatibility-report.js";
|
|
4
4
|
import { buildContractProbes } from "./contract-probes.js";
|
|
5
5
|
import { classifyCompatibilityFixture } from "./fixture-summary.js";
|
|
6
|
-
import { buildIssues, summarizeIssueClasses } from "./issues.js";
|
|
6
|
+
import { buildIssues, isInspectorGapFinding, summarizeIssueClasses } from "./issues.js";
|
|
7
7
|
import { sanitizeReportArtifact } from "./report-sanitizer.js";
|
|
8
8
|
import { applyRuntimeExecutionCoverage } from "./runtime-reconciliation.js";
|
|
9
9
|
|
|
@@ -141,18 +141,24 @@ export async function buildCompatibilityReport(options = {}) {
|
|
|
141
141
|
decisions,
|
|
142
142
|
});
|
|
143
143
|
|
|
144
|
+
const visibleWarnings = filterVisibleFindings(warnings, targetOpenClaw, options);
|
|
145
|
+
const visibleSuggestions = filterVisibleFindings(suggestions, targetOpenClaw, options);
|
|
144
146
|
const runtimeCoverage = applyRuntimeExecutionCoverage({
|
|
145
|
-
findings: [...
|
|
147
|
+
findings: [...visibleWarnings, ...visibleSuggestions],
|
|
146
148
|
executionResults: options.executionResults,
|
|
147
149
|
});
|
|
148
150
|
const issues = buildIssues({
|
|
149
151
|
breakages,
|
|
150
|
-
warnings,
|
|
151
|
-
suggestions,
|
|
152
|
+
warnings: visibleWarnings,
|
|
153
|
+
suggestions: visibleSuggestions,
|
|
152
154
|
targetOpenClaw,
|
|
153
155
|
idPrefix: options.issueIdPrefix,
|
|
154
156
|
});
|
|
155
|
-
const contractProbes = buildContractProbes({
|
|
157
|
+
const contractProbes = buildContractProbes({
|
|
158
|
+
warnings: visibleWarnings,
|
|
159
|
+
suggestions: visibleSuggestions,
|
|
160
|
+
fixtures: fixtureReports,
|
|
161
|
+
});
|
|
156
162
|
const issueSummary = summarizeIssueClasses(issues);
|
|
157
163
|
const openIssues = issues.filter((issue) => issue.status !== "runtime-covered");
|
|
158
164
|
const openIssueSummary = summarizeIssueClasses(openIssues);
|
|
@@ -165,8 +171,8 @@ export async function buildCompatibilityReport(options = {}) {
|
|
|
165
171
|
fixtureCount: fixtureReports.length,
|
|
166
172
|
highPriorityFixtures: fixtureReports.filter((fixture) => fixture.priority === "high").length,
|
|
167
173
|
breakageCount: breakages.length,
|
|
168
|
-
warningCount:
|
|
169
|
-
suggestionCount:
|
|
174
|
+
warningCount: visibleWarnings.length,
|
|
175
|
+
suggestionCount: visibleSuggestions.length,
|
|
170
176
|
decisionCount: decisions.length,
|
|
171
177
|
logCount: logs.length,
|
|
172
178
|
issueCount: issues.length,
|
|
@@ -190,8 +196,8 @@ export async function buildCompatibilityReport(options = {}) {
|
|
|
190
196
|
},
|
|
191
197
|
fixtures: fixtureReports,
|
|
192
198
|
breakages,
|
|
193
|
-
warnings,
|
|
194
|
-
suggestions,
|
|
199
|
+
warnings: visibleWarnings,
|
|
200
|
+
suggestions: visibleSuggestions,
|
|
195
201
|
issues,
|
|
196
202
|
contractProbes,
|
|
197
203
|
logs,
|
|
@@ -199,6 +205,13 @@ export async function buildCompatibilityReport(options = {}) {
|
|
|
199
205
|
};
|
|
200
206
|
}
|
|
201
207
|
|
|
208
|
+
function filterVisibleFindings(findings, targetOpenClaw, options) {
|
|
209
|
+
if (options.includeInspectorGaps === true) {
|
|
210
|
+
return findings;
|
|
211
|
+
}
|
|
212
|
+
return findings.filter((finding) => !isInspectorGapFinding(finding, targetOpenClaw));
|
|
213
|
+
}
|
|
214
|
+
|
|
202
215
|
export function classifyCompatRecordCoverage({ targetOpenClaw, findings, suggestions, logs, decisions }) {
|
|
203
216
|
if (targetOpenClaw.status !== "ok") {
|
|
204
217
|
logs.push({
|
|
@@ -225,6 +238,10 @@ export function classifyCompatRecordCoverage({ targetOpenClaw, findings, suggest
|
|
|
225
238
|
continue;
|
|
226
239
|
}
|
|
227
240
|
|
|
241
|
+
if (finding.code === "sdk-export-missing") {
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
|
|
228
245
|
suggestions.push({
|
|
229
246
|
fixture: finding.fixture,
|
|
230
247
|
code: "missing-compat-record",
|
|
@@ -28,7 +28,7 @@ export function applyRuntimeExecutionCoverage({ findings = [], executionResults
|
|
|
28
28
|
export function buildRuntimeExecutionCoverage(executionResults) {
|
|
29
29
|
const fixtures = new Map();
|
|
30
30
|
for (const artifact of executionResults?.artifacts ?? []) {
|
|
31
|
-
if (artifact.kind
|
|
31
|
+
if (!["capture", "synthetic"].includes(artifact.kind)) {
|
|
32
32
|
continue;
|
|
33
33
|
}
|
|
34
34
|
|
|
@@ -84,6 +84,9 @@ function expectedRuntimeCaptureKeys(finding) {
|
|
|
84
84
|
if (finding.code === "conversation-access-hook") {
|
|
85
85
|
return names.map((name) => `hook:${name}`);
|
|
86
86
|
}
|
|
87
|
+
if (finding.code === "before-tool-call-probe") {
|
|
88
|
+
return ["hook:before_tool_call"];
|
|
89
|
+
}
|
|
87
90
|
return [];
|
|
88
91
|
}
|
|
89
92
|
|