@openclaw/plugin-inspector 0.3.4 → 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,22 +24,48 @@ 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");
38
+ const rssSampleCount = samples.reduce((sum, sample) => sum + (sample.rssSampleCount ?? (sample.peakRssMb > 0 ? 1 : 0)), 0);
39
+ const cpuSampleCount = samples.reduce((sum, sample) => sum + (sample.cpuSampleCount ?? 0), 0);
40
+ const statSampleCount = samples.reduce((sum, sample) => sum + (sample.statSampleCount ?? 0), 0);
33
41
  return {
34
42
  generatedAt: options.generatedAt ?? defaultImportLoopProfileOptions.generatedAt,
35
- mode: options.mode ?? "subprocess-cold-import-loop",
43
+ mode: options.mode ?? "baseline-adjusted-cold-capture-loop",
36
44
  entrypoint,
45
+ baseline,
37
46
  summary: {
38
47
  runs,
48
+ baselineRuns: baseline.runs,
49
+ baselineFailCount: baseline.failCount,
39
50
  p50WallMs: percentile(wallMs, 0.5),
40
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),
41
59
  maxPeakRssMb: Math.max(0, ...samples.map((sample) => sample.peakRssMb)),
42
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,
66
+ statSampleCount,
67
+ rssSampleCount,
68
+ cpuSampleCount,
43
69
  capturedCount: samples.reduce((sum, sample) => sum + sample.capturedCount, 0),
44
70
  failCount: samples.filter((sample) => sample.exitCode !== 0 || sample.status !== "captured").length,
45
71
  },
@@ -52,6 +78,9 @@ export function validateImportLoopProfile(report) {
52
78
  if (report.summary.failCount > 0) {
53
79
  errors.push(`import loop has ${report.summary.failCount} failed sample(s)`);
54
80
  }
81
+ if ((report.summary.baselineFailCount ?? report.baseline?.failCount ?? 0) > 0) {
82
+ errors.push("import loop baseline capture failed");
83
+ }
55
84
  if (report.summary.capturedCount < report.summary.runs) {
56
85
  errors.push("import loop did not capture at least one contract per run");
57
86
  }
@@ -85,7 +114,11 @@ export function renderImportLoopProfileMarkdown(report, options = {}) {
85
114
  "",
86
115
  "## Summary",
87
116
  "",
88
- markdownTable(Object.entries(report.summary).map(([key, value]) => [key, value]), ["Metric", "Value"]),
117
+ markdownTable(summaryRows(report), ["Metric", "Value"]),
118
+ "",
119
+ "## Harness Baseline",
120
+ "",
121
+ markdownTable(baselineRows(report), ["Metric", "Value"]),
89
122
  "",
90
123
  "## Samples",
91
124
  "",
@@ -94,22 +127,132 @@ export function renderImportLoopProfileMarkdown(report, options = {}) {
94
127
  sample.index,
95
128
  sample.status,
96
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"),
97
135
  `${sample.wallMs} ms`,
98
- `${sample.peakRssMb} MB`,
99
- `${sample.cpuMsEstimate} ms`,
136
+ formatSampledMetric(sample.peakRssMb, sample.rssSampleCount),
137
+ formatSampledMetric(sample.cpuMsEstimate, sample.cpuSampleCount, "ms"),
138
+ `${sample.rssSampleCount ?? 0}/${sample.cpuSampleCount ?? 0}`,
100
139
  sample.exitCode,
101
140
  ]),
102
- ["Run", "Status", "Captured", "Wall", "Peak RSS", "CPU Estimate", "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
+ ],
103
156
  ),
104
157
  ].join("\n");
105
158
  }
106
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
+
107
250
  async function runCaptureSample(options) {
108
251
  const outputDir = resolveFromRoot(
109
252
  options.rootDir,
110
253
  options.outputDir ?? defaultImportLoopProfileOptions.outputDir,
111
254
  );
112
- const outputPath = path.join(outputDir, `capture-${options.index}.json`);
255
+ const outputPath = path.join(outputDir, `${options.sampleName ?? "capture"}-${options.index}.json`);
113
256
  await mkdir(path.dirname(outputPath), { recursive: true });
114
257
 
115
258
  const command = buildCaptureCommand({ ...options, outputPath });
@@ -126,14 +269,125 @@ async function runCaptureSample(options) {
126
269
  exitCode: profile.exitCode,
127
270
  status: output?.status ?? "failed",
128
271
  capturedCount: output?.captured?.length ?? 0,
272
+ openClawLifecycle: output?.openClawLifecycle ?? null,
129
273
  wallMs: profile.wallMs,
130
274
  peakRssMb: profile.peakRssMb,
131
275
  peakCpuPercent: profile.peakCpuPercent,
132
276
  cpuMsEstimate: profile.cpuMsEstimate,
277
+ statSampleCount: profile.statSampleCount,
278
+ rssSampleCount: profile.rssSampleCount,
279
+ cpuSampleCount: profile.cpuSampleCount,
133
280
  stderrPreview: profile.stderrPreview,
134
281
  };
135
282
  }
136
283
 
284
+ function summaryRows(report) {
285
+ return [
286
+ ["runs", report.summary.runs],
287
+ ["baselineRuns", report.summary.baselineRuns ?? report.baseline?.runs ?? 0],
288
+ ["baselineFailCount", report.summary.baselineFailCount ?? report.baseline?.failCount ?? 0],
289
+ ["p50WallMs", report.summary.p50WallMs],
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
+ : []),
311
+ ["maxPeakRssMb", formatSampledMetric(report.summary.maxPeakRssMb, report.summary.rssSampleCount)],
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
+ : []),
323
+ ["statSampleCount", report.summary.statSampleCount ?? 0],
324
+ ["rssSampleCount", report.summary.rssSampleCount ?? 0],
325
+ ["cpuSampleCount", report.summary.cpuSampleCount ?? 0],
326
+ ["capturedCount", report.summary.capturedCount],
327
+ ["failCount", report.summary.failCount],
328
+ ];
329
+ }
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
+
348
+ function formatSampledMetric(value, count, unit = "MB") {
349
+ if ((count ?? 0) <= 0) {
350
+ return "n/a";
351
+ }
352
+ return `${value} ${unit}`;
353
+ }
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
+
137
391
  function buildCaptureCommand(options) {
138
392
  if (typeof options.captureCommand === "function") {
139
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
@@ -11,6 +11,9 @@ import { readOpenClawTargetSurface } from "./openclaw-target.js";
11
11
  import { buildCompatibilityReport, buildReport } from "./report.js";
12
12
 
13
13
  const execFileAsync = promisify(execFile);
14
+ const registrationEquivalents = new Map([
15
+ ["registerChannel", new Set(["createChatChannelPlugin", "defineBundledChannelEntry", "defineChannelPluginEntry", "registerChannel"])],
16
+ ]);
14
17
 
15
18
  export async function inspectFixtureSet(config, options = {}) {
16
19
  const { inspections, failures } = await inspectConfiguredFixtures(config, options);
@@ -32,6 +35,7 @@ export async function inspectCompatibilityFixtureSet(config, options = {}) {
32
35
  inspections,
33
36
  failures,
34
37
  generatedAt: options.generatedAt,
38
+ executionResults: options.executionResults,
35
39
  targetOpenClaw,
36
40
  buildFixtureReport: ({ fixture, inspection }) =>
37
41
  buildCompatibilityFixtureReport({
@@ -58,7 +62,7 @@ async function inspectConfiguredFixtures(config, options = {}) {
58
62
  ["manifestContracts", inspection.manifestContracts],
59
63
  ]) {
60
64
  const expected = fixture.expect?.[key] ?? [];
61
- const missing = expected.filter((value) => !observed.includes(value));
65
+ const missing = expected.filter((value) => !satisfiesExpectedSeam(key, value, observed));
62
66
  if (missing.length > 0) {
63
67
  failures.push(`${fixture.id}: missing ${key}: ${missing.join(", ")}`);
64
68
  }
@@ -68,6 +72,17 @@ async function inspectConfiguredFixtures(config, options = {}) {
68
72
  return { inspections, failures };
69
73
  }
70
74
 
75
+ function satisfiesExpectedSeam(key, expected, observed) {
76
+ if (observed.includes(expected)) {
77
+ return true;
78
+ }
79
+ if (key !== "registrations") {
80
+ return false;
81
+ }
82
+ const equivalents = registrationEquivalents.get(expected);
83
+ return Boolean(equivalents && observed.some((value) => equivalents.has(value)));
84
+ }
85
+
71
86
  export async function inspectPlugin(fixture, options = {}) {
72
87
  const config = options.config ?? { rootDir: options.rootDir ?? process.cwd() };
73
88
  const checkoutPath = fixtureCheckoutPath(config, fixture);
@@ -132,6 +147,7 @@ export function inspectSourceText(text, filePath = "source.js") {
132
147
  const hooks = collectDetailedMatches(searchableText, /\bapi\.on\(\s*["'`]([^"'`]+)["'`]/g, filePath, "name");
133
148
  const registrations = [
134
149
  ...collectDetailedMatches(searchableText, /\bapi\.(register[A-Za-z0-9]+)\s*\(/g, filePath, "name"),
150
+ ...collectDetailedMatches(searchableText, /\b(defineBundledChannelEntry)\s*\(/g, filePath, "name"),
135
151
  ...collectDetailedMatches(searchableText, /\b(defineChannelPluginEntry)\s*\(/g, filePath, "name"),
136
152
  ...collectDetailedMatches(searchableText, /\b(createChatChannelPlugin)\s*\(/g, filePath, "name"),
137
153
  ...collectDetailedMatches(searchableText, /\b(definePluginEntry)\s*\(/g, filePath, "name"),
@@ -457,9 +473,38 @@ function lineForOffset(text, offset) {
457
473
  }
458
474
 
459
475
  function stripComments(text) {
460
- return text
461
- .replace(/\/\*[\s\S]*?\*\//g, (comment) => comment.replace(/[^\n]/g, " "))
462
- .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 : " ";
463
508
  }
464
509
 
465
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";