@openclaw/plugin-inspector 0.3.19 → 0.3.21

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 CHANGED
@@ -1,5 +1,27 @@
1
1
  # Changelog
2
2
 
3
+ ## Unreleased
4
+
5
+ ## 0.3.21 - 2026-08-05
6
+
7
+ ### Fixed
8
+
9
+ - Preserve record predicate and coercion contracts in generated SDK mocks so schema-aware plugin imports cannot recurse through primitive values.
10
+ - Read raw plugin manifest fields from packed OpenClaw declarations instead of runtime registry fields, avoiding false unknown-field warnings for supported metadata such as `uiHints`.
11
+
12
+ ## 0.3.20 - 2026-07-31
13
+
14
+ ### Added
15
+
16
+ - Resolve `latest`, `beta`, or exact OpenClaw npm versions for compatibility inspection, reuse prepared targets from cache, and report exact target/source metadata.
17
+
18
+ ### Fixed
19
+
20
+ - Treat version-derived API removals inside a plugin's declared OpenClaw range as author-facing errors while keeping out-of-range targets informational, including beta eligibility against the upcoming stable version.
21
+ - Accept tested minimum OpenClaw host versions below a plugin's build version without reporting package metadata drift.
22
+ - Ignore type-only OpenClaw SDK imports when classifying runtime compatibility against a target release.
23
+ - Accept `openclaw.runtimeSetupEntry` and other paired compiled runtime entrypoints as valid replacements for absent source entrypoints.
24
+
3
25
  ## 0.3.19 - 2026-07-27
4
26
 
5
27
  ### Highlights
package/README.md CHANGED
@@ -34,6 +34,12 @@ checkout. If an OpenClaw checkout is supplied with `--openclaw <path>`, the
34
34
  inspector only reads public compatibility surfaces such as compat records, SDK
35
35
  exports, hook names, manifest fields, and registrar metadata.
36
36
 
37
+ Pass `--openclaw-version latest`, `--openclaw-version beta`, or an exact
38
+ published version to resolve an official OpenClaw npm target. Reports keep the
39
+ exact resolved version and npm source metadata. Prepared targets are cached, so
40
+ batch and repeated inspections do not download the same OpenClaw package for
41
+ each plugin.
42
+
37
43
  ## Quick Start
38
44
 
39
45
  Run this from a plugin package root:
@@ -181,6 +187,7 @@ Common options:
181
187
  | `--config <path>` | Read a standalone config file. Required for fixture-suite `report`. |
182
188
  | `--out <dir>` | Write reports somewhere other than `reports/`. |
183
189
  | `--openclaw <path>` | Compare against a local OpenClaw checkout. |
190
+ | `--openclaw-version latest\|beta\|<exact>` | Resolve and compare against an official OpenClaw npm release. |
184
191
  | `--no-openclaw` | Disable OpenClaw checkout comparison. |
185
192
  | `--runtime` / `--capture` | Add opt-in runtime registration capture. |
186
193
  | `--no-runtime` / `--no-capture` | Disable runtime capture even when config enables it. |
@@ -298,7 +305,7 @@ Important report sections:
298
305
  | --- | --- |
299
306
  | `status` | `pass` unless hard breakages exist. |
300
307
  | `summary` | Counts for fixtures, breakages, warnings, suggestions, issues, issue classes, and contract probes. |
301
- | `targetOpenClaw` | Status and public compatibility data read from the optional OpenClaw checkout. |
308
+ | `targetOpenClaw` | Exact resolved version, source metadata, cache state, and public compatibility data for the selected OpenClaw target. |
302
309
  | `fixtures` | Per-plugin metadata, hooks, registrations, manifest contracts, package data, SDK imports, and SDK deprecation evidence. |
303
310
  | `breakages` | Blocking compatibility failures. |
304
311
  | `warnings` / `suggestions` | Non-blocking compatibility findings. |
@@ -309,8 +316,9 @@ Important report sections:
309
316
 
310
317
  Default `check`, `ci`, and `batch` reports include both author-facing and
311
318
  internal findings. Pass `--author-facing` when producing plugin-author output;
312
- that filtered view includes only findings with `authorRemediation.summary` and
313
- `authorRemediation.docsUrl`.
319
+ that filtered view includes only findings with `authorRemediation.summary`.
320
+ Curated fixes include `authorRemediation.docsUrl`; dynamically discovered API
321
+ removals use generic remediation without inventing a documentation URL.
314
322
  Current author-facing deprecation warnings include deprecated SDK helpers such
315
323
  as `loadSessionStore(...)` when a plugin still depends on the legacy whole-store
316
324
  session shape.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw/plugin-inspector",
3
- "version": "0.3.19",
3
+ "version": "0.3.21",
4
4
  "private": false,
5
5
  "description": "Offline compatibility inspector for OpenClaw plugins.",
6
6
  "type": "module",
@@ -31,6 +31,7 @@
31
31
  "./execution-results": "./src/execution-results.js",
32
32
  "./import-loop-profile": "./src/import-loop-profile.js",
33
33
  "./openclaw-target": "./src/openclaw-target.js",
34
+ "./openclaw-version": "./src/openclaw-version.js",
34
35
  "./platform-probes": "./src/platform-probes.js",
35
36
  "./profile-diff": "./src/profile-diff.js",
36
37
  "./ref-diff": "./src/ref-diff.js",
@@ -69,5 +70,9 @@
69
70
  "plugin",
70
71
  "compatibility",
71
72
  "ci"
72
- ]
73
+ ],
74
+ "dependencies": {
75
+ "semver": "^7.8.5",
76
+ "tar": "^7.5.22"
77
+ }
73
78
  }
package/src/advanced.js CHANGED
@@ -116,6 +116,13 @@ export {
116
116
  parseTypeFields,
117
117
  readOpenClawTargetSurface,
118
118
  } from "./openclaw-target.js";
119
+ export {
120
+ openClawEligibilityVersion,
121
+ prepareOpenClawTarget,
122
+ resolveOpenClawTargetVersion,
123
+ satisfiesOpenClawCompatibilityRange,
124
+ satisfiesOpenClawVersionRange,
125
+ } from "./openclaw-version.js";
119
126
  export {
120
127
  captureEntrypoint,
121
128
  captureEntrypointWithMockSdk,
package/src/api.js CHANGED
@@ -46,6 +46,7 @@ export async function inspectPluginRoot(options = {}) {
46
46
  authorFacing: options.authorFacing,
47
47
  generatedAt: options.generatedAt,
48
48
  openclawPath: options.openclawPath,
49
+ openclawVersion: options.openclawVersion,
49
50
  executionResults: options.executionResults,
50
51
  targetOpenClaw: options.targetOpenClaw,
51
52
  });
@@ -62,6 +63,7 @@ export async function inspectCompatibilityFixtureSetConfig(options = {}) {
62
63
  authorFacing: options.authorFacing,
63
64
  generatedAt: options.generatedAt,
64
65
  openclawPath: options.openclawPath,
66
+ openclawVersion: options.openclawVersion,
65
67
  executionResults: options.executionResults,
66
68
  targetOpenClaw: options.targetOpenClaw,
67
69
  });
@@ -117,6 +119,7 @@ export async function buildFixtureSetColdImportReadiness(options = {}) {
117
119
  authorFacing: options.authorFacing,
118
120
  generatedAt: options.generatedAt,
119
121
  openclawPath: options.openclawPath,
122
+ openclawVersion: options.openclawVersion,
120
123
  executionResults: options.executionResults,
121
124
  targetOpenClaw: options.targetOpenClaw,
122
125
  }));
@@ -150,6 +153,7 @@ export async function buildFixtureSetWorkspacePlan(options = {}) {
150
153
  authorFacing: options.authorFacing,
151
154
  generatedAt: options.generatedAt,
152
155
  openclawPath: options.openclawPath,
156
+ openclawVersion: options.openclawVersion,
153
157
  executionResults: options.executionResults,
154
158
  targetOpenClaw: options.targetOpenClaw,
155
159
  }));
package/src/batch.js CHANGED
@@ -4,6 +4,7 @@ import os from "node:os";
4
4
  import path from "node:path";
5
5
  import { renderMarkdownTable, writeJsonMarkdownArtifacts } from "./artifacts.js";
6
6
  import { runPluginCheck } from "./api.js";
7
+ import { prepareOpenClawTarget, resolveOpenClawTargetVersion } from "./openclaw-version.js";
7
8
 
8
9
  const ignoredDirs = new Set([
9
10
  ".git",
@@ -22,6 +23,11 @@ export async function runBatchAnalysis(options = {}) {
22
23
  const outRoot = path.resolve(rootDir, outDir);
23
24
  const concurrency = Math.max(1, Math.min(Math.round(options.concurrency ?? 4), 32));
24
25
  const keepPluginReports = options.keepPluginReports === true;
26
+ const targetOpenClaw =
27
+ options.targetOpenClaw ??
28
+ (options.openclawVersion
29
+ ? await prepareOpenClawTarget(await resolveOpenClawTargetVersion(options.openclawVersion, options), options)
30
+ : undefined);
25
31
  const pluginRoots = await discoverPluginRoots(rootDir);
26
32
  const tempRoot = keepPluginReports ? null : await mkdtemp(path.join(os.tmpdir(), "plugin-inspector-batch-"));
27
33
  const entries = [];
@@ -37,6 +43,7 @@ export async function runBatchAnalysis(options = {}) {
37
43
  rootDir,
38
44
  outDir: reportsRoot,
39
45
  openclawPath: options.openclawPath,
46
+ targetOpenClaw,
40
47
  }),
41
48
  );
42
49
  });
@@ -113,6 +120,7 @@ async function inspectBatchPlugin(pluginRoot, options) {
113
120
  configPath: options.configPath,
114
121
  mockSdk: options.mockSdk,
115
122
  openclawPath: options.openclawPath,
123
+ targetOpenClaw: options.targetOpenClaw,
116
124
  outDir: options.outDir,
117
125
  pluginRoot,
118
126
  });
package/src/cli.js CHANGED
@@ -57,9 +57,10 @@ try {
57
57
  }
58
58
 
59
59
  async function runBatch(commandArgs) {
60
- const inputDir = readFirstPositional(commandArgs, new Set(["--out", "--openclaw", "--concurrency"]));
60
+ const inputDir = readFirstPositional(commandArgs, new Set(["--out", "--openclaw", "--openclaw-version", "--concurrency"]));
61
61
  const outDir = readFlag(commandArgs, "--out") ?? "reports";
62
62
  const openclawPath = commandArgs.includes("--no-openclaw") ? false : readFlag(commandArgs, "--openclaw");
63
+ const openclawVersion = readOpenClawVersion(commandArgs);
63
64
  const concurrency = Number(readFlag(commandArgs, "--concurrency") ?? "4");
64
65
  const json = commandArgs.includes("--json");
65
66
  const check = commandArgs.includes("--check");
@@ -72,6 +73,7 @@ async function runBatch(commandArgs) {
72
73
  rootDir: inputDir,
73
74
  outDir,
74
75
  openclawPath,
76
+ openclawVersion,
75
77
  concurrency,
76
78
  authorFacing,
77
79
  keepPluginReports,
@@ -105,6 +107,7 @@ async function runCheck(commandArgs, options = {}) {
105
107
  const pluginRoot = readFlag(commandArgs, "--plugin-root") ?? readFlag(commandArgs, "--root");
106
108
  const outDir = readFlag(commandArgs, "--out") ?? "reports";
107
109
  const openclawPath = commandArgs.includes("--no-openclaw") ? false : readFlag(commandArgs, "--openclaw");
110
+ const openclawVersion = readOpenClawVersion(commandArgs);
108
111
  const json = commandArgs.includes("--json");
109
112
  const capture = readRuntimeFlag(commandArgs);
110
113
  const mockSdk = readMockSdkFlag(commandArgs);
@@ -118,6 +121,7 @@ async function runCheck(commandArgs, options = {}) {
118
121
  configPath,
119
122
  mockSdk,
120
123
  openclawPath,
124
+ openclawVersion,
121
125
  outDir,
122
126
  pluginRoot,
123
127
  });
@@ -169,15 +173,17 @@ async function runReport(command, commandArgs) {
169
173
  const configPath = readFlag(commandArgs, "--config");
170
174
  const outDir = readFlag(commandArgs, "--out") ?? "reports";
171
175
  const openclawPath = commandArgs.includes("--no-openclaw") ? false : readFlag(commandArgs, "--openclaw");
176
+ const openclawVersion = readOpenClawVersion(commandArgs);
172
177
  const check = commandArgs.includes("--check") || command === "ci";
173
178
  const json = commandArgs.includes("--json");
174
179
  const ciOutputs = readCiOutputFlags(commandArgs);
175
180
  const authorFacing = readAuthorFacingFlag(commandArgs);
176
181
  const config = await loadInspectorConfig(configPath);
177
- const report = authorFacing
178
- ? await inspectCompatibilityFixtureSet(config, { authorFacing, openclawPath })
182
+ const compatibilityInspection = authorFacing || typeof openclawPath === "string" || openclawVersion !== null;
183
+ const report = compatibilityInspection
184
+ ? await inspectCompatibilityFixtureSet(config, { authorFacing, openclawPath, openclawVersion })
179
185
  : await inspectFixtureSet(config);
180
- const paths = authorFacing
186
+ const paths = compatibilityInspection
181
187
  ? await writeCompatibilityReport(report, { cwd: config.rootDir, outDir })
182
188
  : await writeReport(report, { outDir });
183
189
  await writeCiOutputArtifacts(report, {
@@ -202,6 +208,7 @@ async function runCi(commandArgs) {
202
208
  const pluginRoot = readFlag(commandArgs, "--plugin-root") ?? readFlag(commandArgs, "--root");
203
209
  const outDir = readFlag(commandArgs, "--out") ?? "reports";
204
210
  const openclawPath = commandArgs.includes("--no-openclaw") ? false : readFlag(commandArgs, "--openclaw");
211
+ const openclawVersion = readOpenClawVersion(commandArgs);
205
212
  const json = commandArgs.includes("--json");
206
213
  const capture = readRuntimeFlag(commandArgs);
207
214
  const mockSdk = readMockSdkFlag(commandArgs);
@@ -215,6 +222,7 @@ async function runCi(commandArgs) {
215
222
  configPath,
216
223
  mockSdk,
217
224
  openclawPath,
225
+ openclawVersion,
218
226
  outDir,
219
227
  pluginRoot,
220
228
  });
@@ -256,12 +264,13 @@ async function runCiCompatibilityReport({
256
264
  configPath,
257
265
  mockSdk,
258
266
  openclawPath,
267
+ openclawVersion,
259
268
  outDir,
260
269
  pluginRoot,
261
270
  }) {
262
271
  if (configPath) {
263
272
  const config = await loadInspectorConfig(configPath, { cwd: pluginRoot });
264
- const report = await inspectCompatibilityFixtureSet(config, { authorFacing, openclawPath });
273
+ const report = await inspectCompatibilityFixtureSet(config, { authorFacing, openclawPath, openclawVersion });
265
274
  await writeCompatibilityReport(report, { cwd: config.rootDir, outDir });
266
275
  return {
267
276
  report,
@@ -275,6 +284,7 @@ async function runCiCompatibilityReport({
275
284
  capture,
276
285
  mockSdk,
277
286
  openclawPath,
287
+ openclawVersion,
278
288
  outDir,
279
289
  pluginRoot,
280
290
  });
@@ -314,6 +324,19 @@ function readFlag(commandArgs, name) {
314
324
  return commandArgs[index + 1] ?? null;
315
325
  }
316
326
 
327
+ function readOpenClawVersion(commandArgs) {
328
+ const index = commandArgs.indexOf("--openclaw-version");
329
+ if (index === -1) return null;
330
+ const version = commandArgs[index + 1];
331
+ if (!version || version.startsWith("-")) {
332
+ throw new Error("--openclaw-version requires a value");
333
+ }
334
+ if (version && (commandArgs.includes("--no-openclaw") || commandArgs.includes("--openclaw"))) {
335
+ throw new Error("--openclaw-version cannot be combined with --openclaw or --no-openclaw");
336
+ }
337
+ return version;
338
+ }
339
+
317
340
  function findCaptureEntrypoint(commandArgs) {
318
341
  const flagsWithValues = new Set(["--output", "--plugin-root", "--sdk"]);
319
342
  const consumedIndexes = new Set();
@@ -452,13 +475,13 @@ function printHelp() {
452
475
 
453
476
  Usage:
454
477
  plugin-inspector
455
- plugin-inspector check [--plugin-root <path>] [--config <path>] [--out <dir>] [--openclaw <path>] [--no-openclaw] [--runtime] [--mock-sdk|--real-sdk] [--allow-execute] [--author-facing] [--json]
478
+ plugin-inspector check [--plugin-root <path>] [--config <path>] [--out <dir>] [--openclaw <path> | --openclaw-version latest|beta|<exact> | --no-openclaw] [--runtime] [--mock-sdk|--real-sdk] [--allow-execute] [--author-facing] [--json]
456
479
  plugin-inspector config [--plugin-root <path>] [--config <path>] [--json]
457
480
  plugin-inspector init [--plugin-root <path>] [--config <path>] [--ci] [--scripts] [--package-manager npm|pnpm|yarn|bun] [--dry-run] [--json] [--force]
458
- plugin-inspector report --config <path> [--out <dir>] [--openclaw <path>] [--no-openclaw] [--author-facing] [--check] [--json]
459
- plugin-inspector batch <folder> [--out <dir>] [--openclaw <path>] [--no-openclaw] [--concurrency <n>] [--keep-plugin-reports] [--author-facing] [--check] [--json]
460
- plugin-inspector inspect [--plugin-root <path>] [--config <path>] [--out <dir>] [--openclaw <path>] [--no-openclaw] [--author-facing] [--check] [--json] [--sarif [path]] [--junit [path]] [--allow-execute]
461
- plugin-inspector ci [--plugin-root <path>] [--config <path>] [--out <dir>] [--openclaw <path>] [--no-openclaw] [--runtime] [--mock-sdk|--real-sdk] [--allow-execute] [--author-facing] [--json] [--no-sarif] [--no-junit]
481
+ plugin-inspector report --config <path> [--out <dir>] [--openclaw <path> | --openclaw-version latest|beta|<exact> | --no-openclaw] [--author-facing] [--check] [--json]
482
+ plugin-inspector batch <folder> [--out <dir>] [--openclaw <path> | --openclaw-version latest|beta|<exact> | --no-openclaw] [--concurrency <n>] [--keep-plugin-reports] [--author-facing] [--check] [--json]
483
+ plugin-inspector inspect [--plugin-root <path>] [--config <path>] [--out <dir>] [--openclaw <path> | --openclaw-version latest|beta|<exact> | --no-openclaw] [--author-facing] [--check] [--json] [--sarif [path]] [--junit [path]] [--allow-execute]
484
+ plugin-inspector ci [--plugin-root <path>] [--config <path>] [--out <dir>] [--openclaw <path> | --openclaw-version latest|beta|<exact> | --no-openclaw] [--runtime] [--mock-sdk|--real-sdk] [--allow-execute] [--author-facing] [--json] [--no-sarif] [--no-junit]
462
485
  plugin-inspector capture <entrypoint> [--mock-sdk|--real-sdk] [--allow-execute] [--plugin-root <path>] [--output <path>]
463
486
 
464
487
  Default check runs from the current plugin root and writes reports/ unless --out is set.
@@ -351,6 +351,12 @@ function targetOpenClawTable(targetOpenClaw = {}) {
351
351
  [
352
352
  ["Configured path", targetOpenClaw.configuredPath ?? "-"],
353
353
  ["Status", targetOpenClaw.status],
354
+ ["Requested version", targetOpenClaw.requestedVersion ?? "-"],
355
+ ["Resolved version", targetOpenClaw.version ?? "-"],
356
+ ["Range eligibility version", targetOpenClaw.eligibilityVersion ?? "-"],
357
+ ["Source", targetOpenClaw.source ? `${targetOpenClaw.source.type}:${targetOpenClaw.source.package}` : "-"],
358
+ ["NPM dist-tag", targetOpenClaw.source?.distTag ?? "-"],
359
+ ["Prepared cache", targetOpenClaw.cache ? (targetOpenClaw.cache.hit ? "hit" : "miss") : "-"],
354
360
  ["Compat registry", targetOpenClaw.compatRegistryPath ?? "-"],
355
361
  ["Compat records", targetOpenClaw.compatRecordCount ?? 0],
356
362
  ["Compat status counts", Object.entries(statusCounts).map(([status, count]) => `${status}:${count}`).join(", ") || "-"],
package/src/config.js CHANGED
@@ -195,7 +195,8 @@ async function readJsonIfExists(filePath) {
195
195
  if (!existsSync(filePath)) {
196
196
  return null;
197
197
  }
198
- return JSON.parse(await readFile(filePath, "utf8"));
198
+ const contents = await readFile(filePath, "utf8");
199
+ return JSON.parse(contents.startsWith("\uFEFF") ? contents.slice(1) : contents);
199
200
  }
200
201
 
201
202
  export function packageId(packageName) {
@@ -3,6 +3,7 @@ import { readdir } from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { compatRecordForIssueCode } from "./contract-probes.js";
5
5
  import { readJsonFile } from "./json-file.js";
6
+ import { satisfiesOpenClawCompatibilityRange } from "./openclaw-version.js";
6
7
 
7
8
  const conversationAccessHooks = new Set(["agent_end", "llm_input", "llm_output"]);
8
9
  const captureGapRegistrations = new Set([
@@ -143,6 +144,8 @@ export function summarizePackage(packagePath, packageJson, options = {}) {
143
144
  extensions: arrayValues(packageJson.openclaw.extensions),
144
145
  runtimeExtensions: arrayValues(packageJson.openclaw.runtimeExtensions),
145
146
  setupEntry: typeof packageJson.openclaw.setupEntry === "string" ? packageJson.openclaw.setupEntry : null,
147
+ runtimeSetupEntry:
148
+ typeof packageJson.openclaw.runtimeSetupEntry === "string" ? packageJson.openclaw.runtimeSetupEntry : null,
146
149
  compatPluginApi:
147
150
  typeof packageJson.openclaw.compat?.pluginApi === "string" ? packageJson.openclaw.compat.pluginApi : null,
148
151
  buildOpenClawVersion:
@@ -464,30 +467,36 @@ export function classifyPackageContracts({ fixture, inspection, fixtureReport })
464
467
  }
465
468
 
466
469
  export function classifyTargetOpenClawCoverage({ fixture, inspection, fixtureReport, targetOpenClaw }) {
470
+ const breakages = [];
467
471
  const warnings = [];
472
+ const suggestions = [];
468
473
  const logs = [];
469
474
  const decisions = [];
475
+ const findingContext = { breakages, warnings, suggestions, fixtureReport, targetOpenClaw };
470
476
 
471
- classifyHookNameCoverage({ fixture, inspection, targetOpenClaw, warnings, logs });
472
- classifyRegistrationNameCoverage({ fixture, inspection, targetOpenClaw, warnings, logs });
473
- classifySdkImportCoverage({ fixture, fixtureReport, targetOpenClaw, warnings, logs, decisions });
477
+ classifyHookNameCoverage({ fixture, inspection, targetOpenClaw, logs, findingContext });
478
+ classifyRegistrationNameCoverage({ fixture, inspection, targetOpenClaw, logs, findingContext });
479
+ classifySdkImportCoverage({ fixture, fixtureReport, targetOpenClaw, warnings, logs, decisions, findingContext });
474
480
  classifyManifestFieldCoverage({ fixture, fixtureReport, targetOpenClaw, warnings, logs, decisions });
475
481
 
476
- return { warnings, logs, decisions };
482
+ return { breakages, warnings, suggestions, logs, decisions };
477
483
  }
478
484
 
479
485
  export function classifyCompatibilityFixture({ fixture, inspection, fixtureReport, targetOpenClaw }) {
486
+ const breakages = [];
480
487
  const warnings = [];
481
488
  const suggestions = [];
482
489
  const logs = [];
483
490
  const decisions = [];
484
491
 
485
492
  if (inspection.status !== "ok") {
486
- return { warnings, suggestions, logs, decisions };
493
+ return { breakages, warnings, suggestions, logs, decisions };
487
494
  }
488
495
 
489
496
  const targetCoverage = classifyTargetOpenClawCoverage({ fixture, inspection, fixtureReport, targetOpenClaw });
497
+ breakages.push(...targetCoverage.breakages);
490
498
  warnings.push(...targetCoverage.warnings);
499
+ suggestions.push(...targetCoverage.suggestions);
491
500
  logs.push(...targetCoverage.logs);
492
501
  decisions.push(...targetCoverage.decisions);
493
502
 
@@ -701,7 +710,7 @@ export function classifyCompatibilityFixture({ fixture, inspection, fixtureRepor
701
710
  });
702
711
  }
703
712
 
704
- return { warnings, suggestions, logs, decisions };
713
+ return { breakages, warnings, suggestions, logs, decisions };
705
714
  }
706
715
 
707
716
  function classifySdkDeprecations({ fixture, inspection, fixtureReport, warnings, decisions }) {
@@ -807,7 +816,7 @@ function registrationCaptureGapDetails(inspection, targetOpenClaw) {
807
816
  return apiRegistrationDetails.filter((registration) => captureGapRegistrations.has(registration.name));
808
817
  }
809
818
 
810
- function classifyHookNameCoverage({ fixture, inspection, targetOpenClaw, warnings, logs }) {
819
+ function classifyHookNameCoverage({ fixture, inspection, targetOpenClaw, logs, findingContext }) {
811
820
  if (targetOpenClaw.status !== "ok" || targetOpenClaw.hookNames.length === 0) {
812
821
  return;
813
822
  }
@@ -825,16 +834,18 @@ function classifyHookNameCoverage({ fixture, inspection, targetOpenClaw, warning
825
834
  return;
826
835
  }
827
836
 
828
- warnings.push({
837
+ addVersionDerivedFinding({
838
+ ...findingContext,
839
+ finding: {
829
840
  fixture: fixture.id,
830
841
  code: "unknown-hook-name",
831
- level: "warning",
832
842
  message: "fixture registers hooks that are not present in the target OpenClaw hook registry",
833
843
  evidence: detailEvidence(unknownHooks),
844
+ },
834
845
  });
835
846
  }
836
847
 
837
- function classifyRegistrationNameCoverage({ fixture, inspection, targetOpenClaw, warnings, logs }) {
848
+ function classifyRegistrationNameCoverage({ fixture, inspection, targetOpenClaw, logs, findingContext }) {
838
849
  if (targetOpenClaw.status !== "ok" || targetOpenClaw.apiRegistrars.length === 0) {
839
850
  return;
840
851
  }
@@ -855,16 +866,18 @@ function classifyRegistrationNameCoverage({ fixture, inspection, targetOpenClaw,
855
866
  return;
856
867
  }
857
868
 
858
- warnings.push({
869
+ addVersionDerivedFinding({
870
+ ...findingContext,
871
+ finding: {
859
872
  fixture: fixture.id,
860
873
  code: "unknown-registration-name",
861
- level: "warning",
862
874
  message: "fixture calls api.register* methods that are not present in the target OpenClaw plugin API builder",
863
875
  evidence: detailEvidence(unknownRegistrations),
876
+ },
864
877
  });
865
878
  }
866
879
 
867
- function classifySdkImportCoverage({ fixture, fixtureReport, targetOpenClaw, warnings, logs, decisions }) {
880
+ function classifySdkImportCoverage({ fixture, fixtureReport, targetOpenClaw, warnings, logs, decisions, findingContext }) {
868
881
  if (targetOpenClaw.status !== "ok" || targetOpenClaw.sdkExports.length === 0 || fixtureReport.sdkImports.length === 0) {
869
882
  return;
870
883
  }
@@ -888,13 +901,15 @@ function classifySdkImportCoverage({ fixture, fixtureReport, targetOpenClaw, war
888
901
  }
889
902
 
890
903
  if (unknownImports.length > 0) {
891
- warnings.push({
904
+ addVersionDerivedFinding({
905
+ ...findingContext,
906
+ finding: {
892
907
  fixture: fixture.id,
893
908
  code: "sdk-export-missing",
894
- level: "warning",
895
909
  message: "fixture imports plugin SDK aliases that are not exported by the target OpenClaw package",
896
910
  evidence: detailEvidence(unknownImports, "specifier"),
897
911
  compatRecord: "plugin-sdk-export-aliases",
912
+ },
898
913
  });
899
914
  decisions.push({
900
915
  fixture: fixture.id,
@@ -923,6 +938,52 @@ function classifySdkImportCoverage({ fixture, fixtureReport, targetOpenClaw, war
923
938
  }
924
939
  }
925
940
 
941
+ function addVersionDerivedFinding({ finding, fixtureReport, targetOpenClaw, breakages, warnings, suggestions }) {
942
+ const compatibility = targetCompatibility(fixtureReport, targetOpenClaw);
943
+ if (!compatibility) {
944
+ warnings.push({ ...finding, level: "warning" });
945
+ return;
946
+ }
947
+
948
+ if (compatibility.inDeclaredRange) {
949
+ breakages.push({
950
+ ...finding,
951
+ level: "breakage",
952
+ compatibility,
953
+ authorRemediation: {
954
+ summary:
955
+ "Update the plugin to use APIs available in the target OpenClaw version, or narrow its declared compatibility range.",
956
+ },
957
+ authorRemediationDocs: false,
958
+ });
959
+ return;
960
+ }
961
+
962
+ suggestions.push({
963
+ ...finding,
964
+ level: "information",
965
+ message: `${finding.message}; the resolved target is outside the plugin's declared compatibility range`,
966
+ compatibility,
967
+ });
968
+ }
969
+
970
+ function targetCompatibility(fixtureReport, targetOpenClaw) {
971
+ const declaredRange = fixtureReport.package?.openclaw?.compatPluginApi;
972
+ const targetVersion = targetOpenClaw.version;
973
+ const evaluatedVersion = targetOpenClaw.eligibilityVersion ?? targetOpenClaw.version;
974
+ if (!declaredRange || !targetVersion || !evaluatedVersion || !targetOpenClaw.requestedVersion) return null;
975
+ return {
976
+ declaredRange,
977
+ targetVersion,
978
+ evaluatedVersion,
979
+ inDeclaredRange: satisfiesOpenClawCompatibilityRange({
980
+ targetVersion,
981
+ eligibilityVersion: evaluatedVersion,
982
+ range: declaredRange,
983
+ }),
984
+ };
985
+ }
986
+
926
987
  function classifyManifestFieldCoverage({ fixture, fixtureReport, targetOpenClaw, warnings, logs, decisions }) {
927
988
  if (targetOpenClaw.status !== "ok" || targetOpenClaw.manifestFields.length === 0) {
928
989
  return;
@@ -984,6 +1045,9 @@ function collectOpenClawEntrypoints(packageDir, openclaw, options) {
984
1045
  ...openclaw.extensions.map((specifier) => ({ kind: "extension", specifier })),
985
1046
  ...openclaw.runtimeExtensions.map((specifier) => ({ kind: "runtimeExtension", specifier })),
986
1047
  ...(openclaw.setupEntry ? [{ kind: "setupEntry", specifier: openclaw.setupEntry }] : []),
1048
+ ...(openclaw.runtimeSetupEntry
1049
+ ? [{ kind: "runtimeSetupEntry", specifier: openclaw.runtimeSetupEntry }]
1050
+ : []),
987
1051
  ];
988
1052
 
989
1053
  return entrypoints.map((entrypoint) => {
@@ -1015,7 +1079,7 @@ function hasUsablePackageRuntimeEntrypoint(entrypoint, packageSummary, entrypoin
1015
1079
  return true;
1016
1080
  }
1017
1081
 
1018
- if (entrypoint.kind === "extension" && entrypoints.some((candidate) => candidate.kind === "runtimeExtension" && candidate.exists)) {
1082
+ if (entrypoints.some((candidate) => candidate.exists && isPairedRuntimeEntrypoint(entrypoint, candidate))) {
1019
1083
  return true;
1020
1084
  }
1021
1085
 
@@ -1038,6 +1102,13 @@ function normalizeEntrypointSpecifier(specifier) {
1038
1102
  return normalized.startsWith("./") ? normalized : `./${normalized}`;
1039
1103
  }
1040
1104
 
1105
+ function isPairedRuntimeEntrypoint(entrypoint, candidate) {
1106
+ return (
1107
+ (entrypoint.kind === "extension" && candidate.kind === "runtimeExtension") ||
1108
+ (entrypoint.kind === "setupEntry" && candidate.kind === "runtimeSetupEntry")
1109
+ );
1110
+ }
1111
+
1041
1112
  async function findPackageFiles(root, options, depth = 0) {
1042
1113
  if (!existsSync(root) || depth > options.maxDepth) {
1043
1114
  return [];
@@ -1227,9 +1298,10 @@ function hasPackagedRuntimeEntrypoint(entrypoint, packageSummary, entrypoints) {
1227
1298
  }
1228
1299
 
1229
1300
  if (
1230
- entrypoint.kind === "extension" &&
1231
1301
  entrypoints.some(
1232
- (candidate) => candidate.kind === "runtimeExtension" && repoPathIncludedInNpmPack(packageSummary, candidate.relativePath),
1302
+ (candidate) =>
1303
+ isPairedRuntimeEntrypoint(entrypoint, candidate) &&
1304
+ repoPathIncludedInNpmPack(packageSummary, candidate.relativePath),
1233
1305
  )
1234
1306
  ) {
1235
1307
  return true;
@@ -1245,12 +1317,82 @@ function packageMinHostVersionDrift(packageSummary) {
1245
1317
  if (!nonEmptyString(openclaw?.install?.minHostVersion) || !nonEmptyString(openclaw?.buildOpenClawVersion)) {
1246
1318
  return false;
1247
1319
  }
1248
- return parseMinHostVersionFloor(openclaw.install.minHostVersion) !== openclaw.buildOpenClawVersion;
1320
+ const minimumHostVersion = parseMinHostVersionFloor(openclaw.install.minHostVersion);
1321
+ const buildOpenClawVersion = parseSemver(openclaw.buildOpenClawVersion);
1322
+ if (!minimumHostVersion || !buildOpenClawVersion) {
1323
+ return true;
1324
+ }
1325
+ return compareSemver(minimumHostVersion, buildOpenClawVersion) > 0;
1249
1326
  }
1250
1327
 
1251
1328
  function parseMinHostVersionFloor(value) {
1252
1329
  const match = /^>=([0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?)$/.exec(value);
1253
- return match?.[1] ?? null;
1330
+ return match ? parseSemver(match[1]) : null;
1331
+ }
1332
+
1333
+ function parseSemver(value) {
1334
+ const match =
1335
+ /^([0-9]+)\.([0-9]+)\.([0-9]+)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/.exec(
1336
+ value,
1337
+ );
1338
+ if (!match) {
1339
+ return null;
1340
+ }
1341
+ const coreParts = match.slice(1, 4);
1342
+ if (coreParts.some((part) => part.length > 1 && part.startsWith("0"))) {
1343
+ return null;
1344
+ }
1345
+ const prerelease = match[4]?.split(".") ?? [];
1346
+ if (prerelease.some((part) => /^[0-9]+$/.test(part) && part.length > 1 && part.startsWith("0"))) {
1347
+ return null;
1348
+ }
1349
+ return { core: coreParts, prerelease };
1350
+ }
1351
+
1352
+ function compareNumericIdentifier(left, right) {
1353
+ if (left.length !== right.length) {
1354
+ return left.length < right.length ? -1 : 1;
1355
+ }
1356
+ if (left === right) {
1357
+ return 0;
1358
+ }
1359
+ return left < right ? -1 : 1;
1360
+ }
1361
+
1362
+ function compareSemver(left, right) {
1363
+ for (let index = 0; index < left.core.length; index += 1) {
1364
+ const comparison = compareNumericIdentifier(left.core[index], right.core[index]);
1365
+ if (comparison !== 0) {
1366
+ return comparison;
1367
+ }
1368
+ }
1369
+ if (left.prerelease.length === 0 || right.prerelease.length === 0) {
1370
+ if (left.prerelease.length === right.prerelease.length) {
1371
+ return 0;
1372
+ }
1373
+ return left.prerelease.length === 0 ? 1 : -1;
1374
+ }
1375
+ const count = Math.max(left.prerelease.length, right.prerelease.length);
1376
+ for (let index = 0; index < count; index += 1) {
1377
+ const leftPart = left.prerelease[index];
1378
+ const rightPart = right.prerelease[index];
1379
+ if (leftPart === undefined || rightPart === undefined) {
1380
+ return leftPart === undefined ? -1 : 1;
1381
+ }
1382
+ if (leftPart === rightPart) {
1383
+ continue;
1384
+ }
1385
+ const leftNumeric = /^[0-9]+$/.test(leftPart);
1386
+ const rightNumeric = /^[0-9]+$/.test(rightPart);
1387
+ if (leftNumeric && rightNumeric) {
1388
+ return compareNumericIdentifier(leftPart, rightPart);
1389
+ }
1390
+ if (leftNumeric !== rightNumeric) {
1391
+ return leftNumeric ? -1 : 1;
1392
+ }
1393
+ return leftPart < rightPart ? -1 : 1;
1394
+ }
1395
+ return 0;
1254
1396
  }
1255
1397
 
1256
1398
  function repoPathIncludedInNpmPack(packageSummary, repoPath) {