@openclaw/plugin-inspector 0.3.5 → 0.3.6

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.
@@ -1,4 +1,4 @@
1
- import { mkdir } from "node:fs/promises";
1
+ import { mkdir, writeFile } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
4
  import { renderPaddedMarkdownTable, writeJsonMarkdownArtifacts } from "./artifacts.js";
@@ -24,25 +24,45 @@ export async function buildImportLoopProfile(options = {}) {
24
24
  const entrypoint = options.entrypoint ?? defaultImportLoopProfileOptions.entrypoint;
25
25
  assertRunCount(runs, 20);
26
26
 
27
+ const baseline = await buildBaselineProfile({ ...options, rootDir, runs });
27
28
  const samples = [];
28
29
  for (let index = 0; index < runs; index += 1) {
29
- samples.push(await runCaptureSample({ ...options, entrypoint, index, rootDir }));
30
+ const sample = await runCaptureSample({ ...options, entrypoint, index, rootDir });
31
+ samples.push(applyBaselineAdjustment(sample, baseline));
30
32
  }
31
33
 
32
34
  const wallMs = samples.map((sample) => sample.wallMs).sort((left, right) => left - right);
35
+ const pluginWallDeltaMs = samples.map((sample) => sample.pluginWallDeltaMs).sort((left, right) => left - right);
36
+ const openClawImportMs = openClawLifecycleMetric(samples, "importMs");
37
+ const openClawActivationMs = openClawLifecycleMetric(samples, "activationMs");
33
38
  const rssSampleCount = samples.reduce((sum, sample) => sum + (sample.rssSampleCount ?? (sample.peakRssMb > 0 ? 1 : 0)), 0);
34
39
  const cpuSampleCount = samples.reduce((sum, sample) => sum + (sample.cpuSampleCount ?? 0), 0);
35
40
  const statSampleCount = samples.reduce((sum, sample) => sum + (sample.statSampleCount ?? 0), 0);
36
41
  return {
37
42
  generatedAt: options.generatedAt ?? defaultImportLoopProfileOptions.generatedAt,
38
- mode: options.mode ?? "subprocess-cold-import-loop",
43
+ mode: options.mode ?? "baseline-adjusted-cold-capture-loop",
39
44
  entrypoint,
45
+ baseline,
40
46
  summary: {
41
47
  runs,
48
+ baselineRuns: baseline.runs,
49
+ baselineFailCount: baseline.failCount,
42
50
  p50WallMs: percentile(wallMs, 0.5),
43
51
  p95WallMs: percentile(wallMs, 0.95),
52
+ p50PluginWallDeltaMs: percentile(pluginWallDeltaMs, 0.5),
53
+ p95PluginWallDeltaMs: percentile(pluginWallDeltaMs, 0.95),
54
+ openClawLifecycleCount: openClawImportMs.length,
55
+ p50OpenClawImportMs: percentile(openClawImportMs, 0.5),
56
+ p95OpenClawImportMs: percentile(openClawImportMs, 0.95),
57
+ p50OpenClawActivationMs: percentile(openClawActivationMs, 0.5),
58
+ p95OpenClawActivationMs: percentile(openClawActivationMs, 0.95),
44
59
  maxPeakRssMb: Math.max(0, ...samples.map((sample) => sample.peakRssMb)),
45
60
  maxCpuMsEstimate: Math.max(0, ...samples.map((sample) => sample.cpuMsEstimate)),
61
+ maxPluginPeakRssDeltaMb: Math.max(0, ...samples.map((sample) => sample.pluginPeakRssDeltaMb)),
62
+ maxPluginCpuDeltaMsEstimate: Math.max(0, ...samples.map((sample) => sample.pluginCpuDeltaMsEstimate)),
63
+ baselineReferenceWallMs: baseline.reference.wallMs,
64
+ baselineReferencePeakRssMb: baseline.reference.peakRssMb,
65
+ baselineReferenceCpuMsEstimate: baseline.reference.cpuMsEstimate,
46
66
  statSampleCount,
47
67
  rssSampleCount,
48
68
  cpuSampleCount,
@@ -58,6 +78,9 @@ export function validateImportLoopProfile(report) {
58
78
  if (report.summary.failCount > 0) {
59
79
  errors.push(`import loop has ${report.summary.failCount} failed sample(s)`);
60
80
  }
81
+ if ((report.summary.baselineFailCount ?? report.baseline?.failCount ?? 0) > 0) {
82
+ errors.push("import loop baseline capture failed");
83
+ }
61
84
  if (report.summary.capturedCount < report.summary.runs) {
62
85
  errors.push("import loop did not capture at least one contract per run");
63
86
  }
@@ -93,6 +116,10 @@ export function renderImportLoopProfileMarkdown(report, options = {}) {
93
116
  "",
94
117
  markdownTable(summaryRows(report), ["Metric", "Value"]),
95
118
  "",
119
+ "## Harness Baseline",
120
+ "",
121
+ markdownTable(baselineRows(report), ["Metric", "Value"]),
122
+ "",
96
123
  "## Samples",
97
124
  "",
98
125
  markdownTable(
@@ -100,23 +127,132 @@ export function renderImportLoopProfileMarkdown(report, options = {}) {
100
127
  sample.index,
101
128
  sample.status,
102
129
  sample.capturedCount,
130
+ formatOpenClawLifecycleMetric(sample.openClawLifecycle?.importMs),
131
+ formatOpenClawLifecycleMetric(sample.openClawLifecycle?.activationMs),
132
+ formatOptionalMetric(sample.pluginWallDeltaMs, "ms"),
133
+ formatSampledMetric(sample.pluginPeakRssDeltaMb, sample.rssSampleCount),
134
+ formatSampledMetric(sample.pluginCpuDeltaMsEstimate, sample.cpuSampleCount, "ms"),
103
135
  `${sample.wallMs} ms`,
104
136
  formatSampledMetric(sample.peakRssMb, sample.rssSampleCount),
105
137
  formatSampledMetric(sample.cpuMsEstimate, sample.cpuSampleCount, "ms"),
106
138
  `${sample.rssSampleCount ?? 0}/${sample.cpuSampleCount ?? 0}`,
107
139
  sample.exitCode,
108
140
  ]),
109
- ["Run", "Status", "Captured", "Wall", "Peak RSS", "CPU Estimate", "RSS/CPU samples", "Exit"],
141
+ [
142
+ "Run",
143
+ "Status",
144
+ "Captured",
145
+ "OpenClaw Import",
146
+ "OpenClaw Activate",
147
+ "Plugin Wall Delta",
148
+ "Plugin RSS Delta",
149
+ "Plugin CPU Delta",
150
+ "Raw Wall",
151
+ "Raw Peak RSS",
152
+ "Raw CPU Estimate",
153
+ "RSS/CPU samples",
154
+ "Exit",
155
+ ],
110
156
  ),
111
157
  ].join("\n");
112
158
  }
113
159
 
160
+ async function buildBaselineProfile(options) {
161
+ const baselineRuns = options.baseline === false ? 0 : options.baselineRuns ?? Math.min(options.runs, 3);
162
+ if (baselineRuns <= 0) {
163
+ return emptyBaseline();
164
+ }
165
+
166
+ const entrypoint = await writeBaselineEntrypoint(options);
167
+ const samples = [];
168
+ for (let index = 0; index < baselineRuns; index += 1) {
169
+ samples.push(
170
+ await runCaptureSample({
171
+ ...options,
172
+ entrypoint,
173
+ index,
174
+ sampleName: "baseline",
175
+ rootDir: options.rootDir,
176
+ }),
177
+ );
178
+ }
179
+
180
+ const wallMs = sortedMetric(samples, "wallMs");
181
+ const peakRssMb = sortedMetric(samples, "peakRssMb");
182
+ const cpuMsEstimate = sortedMetric(samples, "cpuMsEstimate");
183
+ return {
184
+ mode: "minimal-plugin-capture",
185
+ runs: baselineRuns,
186
+ entrypoint: path.relative(options.rootDir, entrypoint),
187
+ reference: {
188
+ wallMs: percentile(wallMs, 0.5),
189
+ peakRssMb: percentile(peakRssMb, 0.5),
190
+ cpuMsEstimate: percentile(cpuMsEstimate, 0.5),
191
+ },
192
+ max: {
193
+ wallMs: wallMs.at(-1) ?? 0,
194
+ peakRssMb: peakRssMb.at(-1) ?? 0,
195
+ cpuMsEstimate: cpuMsEstimate.at(-1) ?? 0,
196
+ },
197
+ statSampleCount: samples.reduce((sum, sample) => sum + (sample.statSampleCount ?? 0), 0),
198
+ rssSampleCount: samples.reduce((sum, sample) => sum + (sample.rssSampleCount ?? 0), 0),
199
+ cpuSampleCount: samples.reduce((sum, sample) => sum + (sample.cpuSampleCount ?? 0), 0),
200
+ failCount: samples.filter((sample) => sample.exitCode !== 0 || sample.status !== "captured").length,
201
+ samples,
202
+ };
203
+ }
204
+
205
+ function emptyBaseline() {
206
+ return {
207
+ mode: "disabled",
208
+ runs: 0,
209
+ entrypoint: null,
210
+ reference: {
211
+ wallMs: 0,
212
+ peakRssMb: 0,
213
+ cpuMsEstimate: 0,
214
+ },
215
+ max: {
216
+ wallMs: 0,
217
+ peakRssMb: 0,
218
+ cpuMsEstimate: 0,
219
+ },
220
+ statSampleCount: 0,
221
+ rssSampleCount: 0,
222
+ cpuSampleCount: 0,
223
+ failCount: 0,
224
+ samples: [],
225
+ };
226
+ }
227
+
228
+ async function writeBaselineEntrypoint(options) {
229
+ const outputDir = resolveFromRoot(
230
+ options.rootDir,
231
+ options.outputDir ?? defaultImportLoopProfileOptions.outputDir,
232
+ );
233
+ const baselinePath = path.join(outputDir, "baseline-plugin.mjs");
234
+ await mkdir(path.dirname(baselinePath), { recursive: true });
235
+ await writeFile(
236
+ baselinePath,
237
+ [
238
+ "export default {",
239
+ " register(api) {",
240
+ " api.registerTool({ name: 'baseline_tool', inputSchema: { type: 'object' }, run() {} });",
241
+ " },",
242
+ "};",
243
+ "",
244
+ ].join("\n"),
245
+ "utf8",
246
+ );
247
+ return baselinePath;
248
+ }
249
+
114
250
  async function runCaptureSample(options) {
115
251
  const outputDir = resolveFromRoot(
116
252
  options.rootDir,
117
253
  options.outputDir ?? defaultImportLoopProfileOptions.outputDir,
118
254
  );
119
- const outputPath = path.join(outputDir, `capture-${options.index}.json`);
255
+ const outputPath = path.join(outputDir, `${options.sampleName ?? "capture"}-${options.index}.json`);
120
256
  await mkdir(path.dirname(outputPath), { recursive: true });
121
257
 
122
258
  const command = buildCaptureCommand({ ...options, outputPath });
@@ -133,6 +269,7 @@ async function runCaptureSample(options) {
133
269
  exitCode: profile.exitCode,
134
270
  status: output?.status ?? "failed",
135
271
  capturedCount: output?.captured?.length ?? 0,
272
+ openClawLifecycle: output?.openClawLifecycle ?? null,
136
273
  wallMs: profile.wallMs,
137
274
  peakRssMb: profile.peakRssMb,
138
275
  peakCpuPercent: profile.peakCpuPercent,
@@ -147,10 +284,42 @@ async function runCaptureSample(options) {
147
284
  function summaryRows(report) {
148
285
  return [
149
286
  ["runs", report.summary.runs],
287
+ ["baselineRuns", report.summary.baselineRuns ?? report.baseline?.runs ?? 0],
288
+ ["baselineFailCount", report.summary.baselineFailCount ?? report.baseline?.failCount ?? 0],
150
289
  ["p50WallMs", report.summary.p50WallMs],
151
290
  ["p95WallMs", report.summary.p95WallMs],
291
+ ...(Number.isFinite(report.summary.p50PluginWallDeltaMs)
292
+ ? [
293
+ ["p50PluginWallDeltaMs", report.summary.p50PluginWallDeltaMs],
294
+ ["p95PluginWallDeltaMs", report.summary.p95PluginWallDeltaMs],
295
+ ["maxPluginPeakRssDeltaMb", formatSampledMetric(report.summary.maxPluginPeakRssDeltaMb, report.summary.rssSampleCount)],
296
+ [
297
+ "maxPluginCpuDeltaMsEstimate",
298
+ formatSampledMetric(report.summary.maxPluginCpuDeltaMsEstimate, report.summary.cpuSampleCount, "ms"),
299
+ ],
300
+ ]
301
+ : []),
302
+ ...((report.summary.openClawLifecycleCount ?? 0) > 0
303
+ ? [
304
+ ["openClawLifecycleCount", report.summary.openClawLifecycleCount],
305
+ ["p50OpenClawImportMs", `${report.summary.p50OpenClawImportMs} ms`],
306
+ ["p95OpenClawImportMs", `${report.summary.p95OpenClawImportMs} ms`],
307
+ ["p50OpenClawActivationMs", `${report.summary.p50OpenClawActivationMs} ms`],
308
+ ["p95OpenClawActivationMs", `${report.summary.p95OpenClawActivationMs} ms`],
309
+ ]
310
+ : []),
152
311
  ["maxPeakRssMb", formatSampledMetric(report.summary.maxPeakRssMb, report.summary.rssSampleCount)],
153
312
  ["maxCpuMsEstimate", formatSampledMetric(report.summary.maxCpuMsEstimate, report.summary.cpuSampleCount, "ms")],
313
+ ...(Number.isFinite(report.summary.baselineReferenceWallMs)
314
+ ? [
315
+ ["baselineReferenceWallMs", `${report.summary.baselineReferenceWallMs} ms`],
316
+ ["baselineReferencePeakRssMb", formatSampledMetric(report.summary.baselineReferencePeakRssMb, report.baseline?.rssSampleCount ?? 0)],
317
+ [
318
+ "baselineReferenceCpuMsEstimate",
319
+ formatSampledMetric(report.summary.baselineReferenceCpuMsEstimate, report.baseline?.cpuSampleCount ?? 0, "ms"),
320
+ ],
321
+ ]
322
+ : []),
154
323
  ["statSampleCount", report.summary.statSampleCount ?? 0],
155
324
  ["rssSampleCount", report.summary.rssSampleCount ?? 0],
156
325
  ["cpuSampleCount", report.summary.cpuSampleCount ?? 0],
@@ -159,6 +328,23 @@ function summaryRows(report) {
159
328
  ];
160
329
  }
161
330
 
331
+ function baselineRows(report) {
332
+ const baseline = report.baseline ?? emptyBaseline();
333
+ return [
334
+ ["mode", baseline.mode],
335
+ ["runs", baseline.runs],
336
+ ["entrypoint", baseline.entrypoint ?? "-"],
337
+ ["referenceWallMs", `${baseline.reference?.wallMs ?? 0} ms`],
338
+ ["referencePeakRssMb", formatSampledMetric(baseline.reference?.peakRssMb ?? 0, baseline.rssSampleCount)],
339
+ ["referenceCpuMsEstimate", formatSampledMetric(baseline.reference?.cpuMsEstimate ?? 0, baseline.cpuSampleCount, "ms")],
340
+ ["maxWallMs", `${baseline.max?.wallMs ?? 0} ms`],
341
+ ["maxPeakRssMb", formatSampledMetric(baseline.max?.peakRssMb ?? 0, baseline.rssSampleCount)],
342
+ ["maxCpuMsEstimate", formatSampledMetric(baseline.max?.cpuMsEstimate ?? 0, baseline.cpuSampleCount, "ms")],
343
+ ["statSampleCount", baseline.statSampleCount ?? 0],
344
+ ["failCount", baseline.failCount ?? 0],
345
+ ];
346
+ }
347
+
162
348
  function formatSampledMetric(value, count, unit = "MB") {
163
349
  if ((count ?? 0) <= 0) {
164
350
  return "n/a";
@@ -166,6 +352,42 @@ function formatSampledMetric(value, count, unit = "MB") {
166
352
  return `${value} ${unit}`;
167
353
  }
168
354
 
355
+ function formatOptionalMetric(value, unit) {
356
+ if (!Number.isFinite(value)) {
357
+ return "n/a";
358
+ }
359
+ return `${value} ${unit}`;
360
+ }
361
+
362
+ function formatOpenClawLifecycleMetric(value) {
363
+ return Number.isFinite(value) ? `${value} ms` : "n/a";
364
+ }
365
+
366
+ function openClawLifecycleMetric(samples, field) {
367
+ return samples
368
+ .map((sample) => sample.openClawLifecycle?.[field])
369
+ .filter((value) => Number.isFinite(value))
370
+ .sort((left, right) => left - right);
371
+ }
372
+
373
+ function applyBaselineAdjustment(sample, baseline) {
374
+ return {
375
+ ...sample,
376
+ pluginWallDeltaMs: roundNonNegative(sample.wallMs - baseline.reference.wallMs, 0),
377
+ pluginPeakRssDeltaMb: roundNonNegative(sample.peakRssMb - baseline.reference.peakRssMb, 1),
378
+ pluginCpuDeltaMsEstimate: roundNonNegative(sample.cpuMsEstimate - baseline.reference.cpuMsEstimate, 0),
379
+ };
380
+ }
381
+
382
+ function sortedMetric(samples, field) {
383
+ return samples.map((sample) => sample[field]).sort((left, right) => left - right);
384
+ }
385
+
386
+ function roundNonNegative(value, digits) {
387
+ const scale = 10 ** digits;
388
+ return Math.max(0, Math.round(value * scale) / scale);
389
+ }
390
+
169
391
  function buildCaptureCommand(options) {
170
392
  if (typeof options.captureCommand === "function") {
171
393
  return options.captureCommand({
package/src/index.js CHANGED
@@ -13,6 +13,7 @@ import * as profileDiffApi from "./profile-diff.js";
13
13
  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
+ import * as runtimeReconciliationApi from "./runtime-reconciliation.js";
16
17
  import * as syntheticProbeSuiteApi from "./synthetic-probe-suite.js";
17
18
  import * as syntheticProbesApi from "./synthetic-probes.js";
18
19
 
@@ -111,6 +112,8 @@ export const runtime = Object.freeze({
111
112
  writeImportLoopProfile: importLoopProfileApi.writeImportLoopProfile,
112
113
  renderImportLoopProfile: importLoopProfileApi.renderImportLoopProfileMarkdown,
113
114
  validateImportLoopProfile: importLoopProfileApi.validateImportLoopProfile,
115
+ applyExecutionCoverage: runtimeReconciliationApi.applyRuntimeExecutionCoverage,
116
+ buildExecutionCoverage: runtimeReconciliationApi.buildRuntimeExecutionCoverage,
114
117
  });
115
118
 
116
119
  export const synthetic = Object.freeze({
@@ -227,6 +230,10 @@ export {
227
230
  validateRuntimeProfile,
228
231
  writeRuntimeProfile,
229
232
  } from "./runtime-profile.js";
233
+ export {
234
+ applyRuntimeExecutionCoverage,
235
+ buildRuntimeExecutionCoverage,
236
+ } from "./runtime-reconciliation.js";
230
237
  export { buildSyntheticProbePlanFromReport } from "./synthetic-probe-suite.js";
231
238
  export {
232
239
  buildSyntheticProbePlan,
package/src/inspector.js CHANGED
@@ -12,7 +12,7 @@ import { buildCompatibilityReport, buildReport } from "./report.js";
12
12
 
13
13
  const execFileAsync = promisify(execFile);
14
14
  const registrationEquivalents = new Map([
15
- ["registerChannel", new Set(["createChatChannelPlugin", "defineChannelPluginEntry", "registerChannel"])],
15
+ ["registerChannel", new Set(["createChatChannelPlugin", "defineBundledChannelEntry", "defineChannelPluginEntry", "registerChannel"])],
16
16
  ]);
17
17
 
18
18
  export async function inspectFixtureSet(config, options = {}) {
@@ -35,6 +35,7 @@ export async function inspectCompatibilityFixtureSet(config, options = {}) {
35
35
  inspections,
36
36
  failures,
37
37
  generatedAt: options.generatedAt,
38
+ executionResults: options.executionResults,
38
39
  targetOpenClaw,
39
40
  buildFixtureReport: ({ fixture, inspection }) =>
40
41
  buildCompatibilityFixtureReport({
@@ -146,6 +147,7 @@ export function inspectSourceText(text, filePath = "source.js") {
146
147
  const hooks = collectDetailedMatches(searchableText, /\bapi\.on\(\s*["'`]([^"'`]+)["'`]/g, filePath, "name");
147
148
  const registrations = [
148
149
  ...collectDetailedMatches(searchableText, /\bapi\.(register[A-Za-z0-9]+)\s*\(/g, filePath, "name"),
150
+ ...collectDetailedMatches(searchableText, /\b(defineBundledChannelEntry)\s*\(/g, filePath, "name"),
149
151
  ...collectDetailedMatches(searchableText, /\b(defineChannelPluginEntry)\s*\(/g, filePath, "name"),
150
152
  ...collectDetailedMatches(searchableText, /\b(createChatChannelPlugin)\s*\(/g, filePath, "name"),
151
153
  ...collectDetailedMatches(searchableText, /\b(definePluginEntry)\s*\(/g, filePath, "name"),
@@ -471,9 +473,38 @@ function lineForOffset(text, offset) {
471
473
  }
472
474
 
473
475
  function stripComments(text) {
474
- return text
475
- .replace(/\/\*[\s\S]*?\*\//g, (comment) => comment.replace(/[^\n]/g, " "))
476
- .replace(/\/\/.*$/gm, (comment) => " ".repeat(comment.length));
476
+ let result = "";
477
+ for (let index = 0; index < text.length; index += 1) {
478
+ const char = text[index];
479
+ const next = text[index + 1];
480
+ if (char === "/" && next === "*") {
481
+ result += " ";
482
+ index += 2;
483
+ while (index < text.length && !(text[index] === "*" && text[index + 1] === "/")) {
484
+ result += blankCommentChar(text[index]);
485
+ index += 1;
486
+ }
487
+ if (index < text.length) {
488
+ result += " ";
489
+ index += 1;
490
+ }
491
+ } else if (char === "/" && next === "/") {
492
+ result += " ";
493
+ index += 2;
494
+ while (index < text.length && text[index] !== "\n" && text[index] !== "\r") {
495
+ result += " ";
496
+ index += 1;
497
+ }
498
+ index -= 1;
499
+ } else {
500
+ result += char;
501
+ }
502
+ }
503
+ return result;
504
+ }
505
+
506
+ function blankCommentChar(char) {
507
+ return char === "\n" || char === "\r" ? char : " ";
477
508
  }
478
509
 
479
510
  function sortDetails(details) {
package/src/issues.js CHANGED
@@ -23,17 +23,25 @@ export const knownIssueCodes = new Set([
23
23
  "package-build-artifact-entrypoint",
24
24
  "package-dependency-install-required",
25
25
  "package-entrypoint-missing",
26
+ "package-install-metadata-incomplete",
26
27
  "package-json-missing",
27
28
  "package-manifest-version-drift",
29
+ "package-min-host-version-drift",
30
+ "package-npm-pack-entrypoint-missing",
31
+ "package-npm-pack-metadata-missing",
32
+ "package-npm-pack-unavailable",
28
33
  "package-openclaw-entry-missing",
29
34
  "package-openclaw-metadata-missing",
35
+ "package-openclaw-unsupported-metadata",
30
36
  "package-plugin-api-compat-missing",
31
37
  "package-typescript-source-entrypoint",
32
38
  "provider-auth-env-vars",
33
39
  "registration-capture-gap",
34
40
  "runtime-tool-capture",
35
41
  "reserved-sdk-import",
42
+ "security-manifest-schema-unavailable",
36
43
  "sdk-export-missing",
44
+ "unrecognized-security-manifest",
37
45
  ]);
38
46
 
39
47
  export const issueMetadataByCode = {
@@ -85,6 +93,12 @@ export const issueMetadataByCode = {
85
93
  decision: "plugin-upstream-fix",
86
94
  title: "plugin imports reserved bundled-plugin SDK compatibility subpaths",
87
95
  },
96
+ "security-manifest-schema-unavailable": {
97
+ severity: "P3",
98
+ owner: "plugin",
99
+ decision: "plugin-upstream-fix",
100
+ title: "plugin security manifest references an unavailable schema",
101
+ },
88
102
  "missing-compat-record": {
89
103
  severity: "P1",
90
104
  owner: "core",
@@ -127,6 +141,12 @@ export const issueMetadataByCode = {
127
141
  decision: "plugin-upstream-fix",
128
142
  title: "OpenClaw package entrypoint is missing",
129
143
  },
144
+ "package-install-metadata-incomplete": {
145
+ severity: "P2",
146
+ owner: "plugin",
147
+ decision: "plugin-upstream-fix",
148
+ title: "OpenClaw package install metadata is incomplete",
149
+ },
130
150
  "package-json-missing": {
131
151
  severity: "P2",
132
152
  owner: "plugin",
@@ -139,6 +159,30 @@ export const issueMetadataByCode = {
139
159
  decision: "plugin-upstream-fix",
140
160
  title: "package and manifest versions drift",
141
161
  },
162
+ "package-min-host-version-drift": {
163
+ severity: "P2",
164
+ owner: "plugin",
165
+ decision: "plugin-upstream-fix",
166
+ title: "OpenClaw package minimum host version drifts from build target",
167
+ },
168
+ "package-npm-pack-entrypoint-missing": {
169
+ severity: "P1",
170
+ owner: "plugin",
171
+ decision: "plugin-upstream-fix",
172
+ title: "advertised npm artifact is missing OpenClaw entrypoints",
173
+ },
174
+ "package-npm-pack-metadata-missing": {
175
+ severity: "P2",
176
+ owner: "plugin",
177
+ decision: "plugin-upstream-fix",
178
+ title: "advertised npm artifact is missing OpenClaw metadata",
179
+ },
180
+ "package-npm-pack-unavailable": {
181
+ severity: "P1",
182
+ owner: "plugin",
183
+ decision: "plugin-upstream-fix",
184
+ title: "advertised npm artifact cannot be packed",
185
+ },
142
186
  "package-openclaw-entry-missing": {
143
187
  severity: "P2",
144
188
  owner: "plugin",
@@ -151,6 +195,12 @@ export const issueMetadataByCode = {
151
195
  decision: "plugin-upstream-fix",
152
196
  title: "OpenClaw package metadata is missing",
153
197
  },
198
+ "package-openclaw-unsupported-metadata": {
199
+ severity: "P2",
200
+ owner: "plugin",
201
+ decision: "plugin-upstream-fix",
202
+ title: "package declares unsupported OpenClaw metadata",
203
+ },
154
204
  "package-plugin-api-compat-missing": {
155
205
  severity: "P2",
156
206
  owner: "plugin",
@@ -193,6 +243,12 @@ export const issueMetadataByCode = {
193
243
  decision: "core-compat-adapter",
194
244
  title: "fixture calls a registrar missing from target OpenClaw",
195
245
  },
246
+ "unrecognized-security-manifest": {
247
+ severity: "P3",
248
+ owner: "plugin",
249
+ decision: "plugin-upstream-fix",
250
+ title: "plugin ships an unsupported security manifest",
251
+ },
196
252
  };
197
253
 
198
254
  export function buildIssues({ breakages = [], warnings = [], suggestions = [], targetOpenClaw, idPrefix = "CRABPOT" }) {
@@ -212,7 +268,7 @@ export function buildIssues({ breakages = [], warnings = [], suggestions = [], t
212
268
  owner: finding.owner,
213
269
  code: finding.code,
214
270
  decision: finding.decision,
215
- status: finding.severity === "P0" || finding.level === "breakage" ? "blocking" : "open",
271
+ status: finding.status ?? (finding.severity === "P0" || finding.level === "breakage" ? "blocking" : "open"),
216
272
  issueClass: finding.issueClass,
217
273
  live: finding.live,
218
274
  deprecated: finding.deprecated,
@@ -220,6 +276,7 @@ export function buildIssues({ breakages = [], warnings = [], suggestions = [], t
220
276
  title: issueTitle(finding),
221
277
  evidence: finding.evidence ?? [],
222
278
  compatRecord: finding.compatRecord ?? null,
279
+ runtimeCoverage: finding.runtimeCoverage ?? null,
223
280
  }));
224
281
  }
225
282
 
@@ -314,10 +371,18 @@ function issueClassFor(code, options) {
314
371
  "manifest-unknown-fields",
315
372
  "package-json-missing",
316
373
  "package-manifest-version-drift",
374
+ "package-min-host-version-drift",
375
+ "package-npm-pack-entrypoint-missing",
376
+ "package-npm-pack-metadata-missing",
377
+ "package-npm-pack-unavailable",
317
378
  "package-openclaw-entry-missing",
318
379
  "package-openclaw-metadata-missing",
380
+ "package-openclaw-unsupported-metadata",
319
381
  "package-plugin-api-compat-missing",
382
+ "package-install-metadata-incomplete",
320
383
  "reserved-sdk-import",
384
+ "security-manifest-schema-unavailable",
385
+ "unrecognized-security-manifest",
321
386
  ].includes(code)
322
387
  ) {
323
388
  return "upstream-metadata";
@@ -106,12 +106,89 @@ export function openClawTargetPathCandidates(manifest, configuredPath) {
106
106
 
107
107
  export function parseCompatRecordEntries(source) {
108
108
  const entries = [];
109
- for (const match of source.matchAll(/\{[\s\S]*?\bcode:\s*["'`]([^"'`]+)["'`][\s\S]*?\bstatus:\s*["'`]([^"'`]+)["'`][\s\S]*?\}/g)) {
110
- entries.push({ code: match[1], status: match[2] });
109
+ let cursor = 0;
110
+ while (cursor < source.length) {
111
+ const codeProperty = readStringProperty(source, "code", cursor);
112
+ if (!codeProperty) {
113
+ break;
114
+ }
115
+
116
+ const statusProperty = readStringProperty(source, "status", codeProperty.end);
117
+ if (statusProperty) {
118
+ entries.push({ code: codeProperty.value, status: statusProperty.value });
119
+ cursor = statusProperty.end;
120
+ } else {
121
+ cursor = codeProperty.end;
122
+ }
111
123
  }
112
124
  return dedupeBy(entries, (entry) => entry.code).sort((left, right) => left.code.localeCompare(right.code));
113
125
  }
114
126
 
127
+ function readStringProperty(source, property, fromIndex) {
128
+ const propertyIndex = findProperty(source, property, fromIndex);
129
+ if (propertyIndex === -1) {
130
+ return null;
131
+ }
132
+ const colonIndex = source.indexOf(":", propertyIndex + property.length);
133
+ if (colonIndex === -1) {
134
+ return null;
135
+ }
136
+ let quoteIndex = colonIndex + 1;
137
+ while (quoteIndex < source.length && isWhitespace(source[quoteIndex])) {
138
+ quoteIndex += 1;
139
+ }
140
+ if (!isQuote(source[quoteIndex])) {
141
+ return null;
142
+ }
143
+ return readQuotedValue(source, quoteIndex);
144
+ }
145
+
146
+ function findProperty(source, property, fromIndex) {
147
+ let index = source.indexOf(property, fromIndex);
148
+ while (index !== -1) {
149
+ const previous = index === 0 ? "" : source[index - 1];
150
+ const next = source[index + property.length] ?? "";
151
+ if (!isIdentifierChar(previous) && !isIdentifierChar(next)) {
152
+ return index;
153
+ }
154
+ index = source.indexOf(property, index + property.length);
155
+ }
156
+ return -1;
157
+ }
158
+
159
+ function readQuotedValue(source, quoteIndex) {
160
+ const quote = source[quoteIndex];
161
+ let value = "";
162
+ for (let index = quoteIndex + 1; index < source.length; index += 1) {
163
+ const char = source[index];
164
+ if (char === "\\") {
165
+ value += source[index + 1] ?? "";
166
+ index += 1;
167
+ } else if (char === quote) {
168
+ return { value, end: index + 1 };
169
+ } else {
170
+ value += char;
171
+ }
172
+ }
173
+ return null;
174
+ }
175
+
176
+ function isQuote(char) {
177
+ return char === '"' || char === "'" || char === "`";
178
+ }
179
+
180
+ function isIdentifierChar(char) {
181
+ if (char === "_" || char === "$") {
182
+ return true;
183
+ }
184
+ const code = char.charCodeAt(0);
185
+ return (code >= 48 && code <= 57) || (code >= 65 && code <= 90) || (code >= 97 && code <= 122);
186
+ }
187
+
188
+ function isWhitespace(char) {
189
+ return char === " " || char === "\n" || char === "\r" || char === "\t";
190
+ }
191
+
115
192
  export function parsePluginSdkExports(packageJson) {
116
193
  return Object.keys(packageJson.exports ?? {})
117
194
  .filter((specifier) => specifier === "./plugin-sdk" || specifier.startsWith("./plugin-sdk/"))