@openclaw/plugin-inspector 0.0.0 → 0.1.0
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 +15 -0
- package/LICENSE +21 -0
- package/README.md +249 -2
- package/examples/github-actions-plugin-inspector.yml +24 -0
- package/examples/plugin-inspector.config.json +15 -0
- package/package.json +55 -3
- package/src/artifacts.js +113 -0
- package/src/capture-api.js +115 -0
- package/src/ci-policy.js +259 -0
- package/src/ci-summary.js +223 -0
- package/src/cli.js +130 -0
- package/src/cold-import-readiness.js +271 -0
- package/src/compatibility-report.js +356 -0
- package/src/config.js +182 -0
- package/src/contract-capture.js +288 -0
- package/src/contract-coverage.js +167 -0
- package/src/contract-probes.js +156 -0
- package/src/execution-results.js +297 -0
- package/src/fixture-summary.js +780 -0
- package/src/import-loop-profile.js +169 -0
- package/src/index.js +186 -0
- package/src/inspector.js +405 -0
- package/src/issues.js +366 -0
- package/src/json-file.js +10 -0
- package/src/mock-sdk-capture-runner.js +69 -0
- package/src/openclaw-target.js +179 -0
- package/src/path-utils.js +28 -0
- package/src/platform-probes.js +238 -0
- package/src/process-profile.js +117 -0
- package/src/profile-diff.js +222 -0
- package/src/ref-diff.js +335 -0
- package/src/report.js +435 -0
- package/src/runtime-capture-report.js +134 -0
- package/src/runtime-profile.js +289 -0
- package/src/sdk-mock.js +61 -0
- package/src/stats.js +13 -0
- package/src/synthetic-probes.js +544 -0
- package/src/workspace-plan.js +496 -0
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { renderPaddedMarkdownTable, writeJsonMarkdownArtifacts } from "./artifacts.js";
|
|
4
|
+
import { slugForArtifact } from "./path-utils.js";
|
|
5
|
+
|
|
6
|
+
export function buildColdImportReadiness(options = {}) {
|
|
7
|
+
const report = options.report;
|
|
8
|
+
if (!report) {
|
|
9
|
+
throw new TypeError("buildColdImportReadiness requires a compatibility report");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const rootDir = path.resolve(options.rootDir ?? process.cwd());
|
|
13
|
+
const sdkExports = new Set(report.targetOpenClaw.sdkExports ?? []);
|
|
14
|
+
const fixtures = [];
|
|
15
|
+
|
|
16
|
+
for (const fixture of report.fixtures) {
|
|
17
|
+
const sdkBlockers = (fixture.sdkImportDetails ?? [])
|
|
18
|
+
.filter((sdkImport) => !sdkExports.has(sdkImport.specifier))
|
|
19
|
+
.map((sdkImport) => `${sdkImport.specifier} @ ${sdkImport.ref}`);
|
|
20
|
+
const entrypoints = (fixture.packages ?? []).flatMap((packageSummary) =>
|
|
21
|
+
(packageSummary.openclaw?.entrypoints ?? []).map((entrypoint) =>
|
|
22
|
+
classifyEntrypointReadiness({
|
|
23
|
+
fixture,
|
|
24
|
+
packageSummary,
|
|
25
|
+
entrypoint,
|
|
26
|
+
rootDir,
|
|
27
|
+
sdkBlockers,
|
|
28
|
+
}),
|
|
29
|
+
),
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
fixtures.push({
|
|
33
|
+
id: fixture.id,
|
|
34
|
+
priority: fixture.priority,
|
|
35
|
+
entrypoints,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const allEntrypoints = fixtures.flatMap((fixture) => fixture.entrypoints);
|
|
40
|
+
return {
|
|
41
|
+
generatedAt: report.generatedAt,
|
|
42
|
+
targetOpenClaw: {
|
|
43
|
+
status: report.targetOpenClaw.status,
|
|
44
|
+
configuredPath: report.targetOpenClaw.configuredPath,
|
|
45
|
+
sdkExportCount: report.targetOpenClaw.sdkExportCount ?? 0,
|
|
46
|
+
},
|
|
47
|
+
summary: {
|
|
48
|
+
fixtureCount: fixtures.length,
|
|
49
|
+
entrypointCount: allEntrypoints.length,
|
|
50
|
+
readyCount: allEntrypoints.filter((entrypoint) => entrypoint.status === "ready").length,
|
|
51
|
+
blockedCount: allEntrypoints.filter((entrypoint) => entrypoint.status !== "ready").length,
|
|
52
|
+
tsLoaderRequiredCount: allEntrypoints.filter((entrypoint) => entrypoint.status === "ts-loader-required").length,
|
|
53
|
+
buildRequiredCount: allEntrypoints.filter((entrypoint) => entrypoint.status === "build-required").length,
|
|
54
|
+
dependencyInstallRequiredCount: allEntrypoints.filter((entrypoint) =>
|
|
55
|
+
entrypoint.blockers.some((blocker) => blocker.code === "dependency-install-required"),
|
|
56
|
+
).length,
|
|
57
|
+
sdkAliasRequiredCount: allEntrypoints.filter((entrypoint) =>
|
|
58
|
+
entrypoint.blockers.some((blocker) => blocker.code === "sdk-alias-required"),
|
|
59
|
+
).length,
|
|
60
|
+
},
|
|
61
|
+
fixtures,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function validateColdImportReadiness(readiness) {
|
|
66
|
+
const errors = [];
|
|
67
|
+
|
|
68
|
+
for (const fixture of readiness.fixtures) {
|
|
69
|
+
for (const entrypoint of fixture.entrypoints) {
|
|
70
|
+
if (!entrypoint.id || !entrypoint.path) {
|
|
71
|
+
errors.push(`${fixture.id}: entrypoint is missing id or path`);
|
|
72
|
+
}
|
|
73
|
+
if (!entrypoint.status) {
|
|
74
|
+
errors.push(`${entrypoint.id}: missing readiness status`);
|
|
75
|
+
}
|
|
76
|
+
if (!Array.isArray(entrypoint.assertions) || entrypoint.assertions.length === 0) {
|
|
77
|
+
errors.push(`${entrypoint.id}: missing cold-import assertions`);
|
|
78
|
+
}
|
|
79
|
+
if (entrypoint.status !== "ready" && entrypoint.blockers.length === 0) {
|
|
80
|
+
errors.push(`${entrypoint.id}: blocked entrypoint has no blockers`);
|
|
81
|
+
}
|
|
82
|
+
for (const blocker of entrypoint.blockers) {
|
|
83
|
+
if (!blocker.code || !blocker.evidence) {
|
|
84
|
+
errors.push(`${entrypoint.id}: blocker is missing code or evidence`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return errors;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export async function writeColdImportReadiness(readiness, options = {}) {
|
|
94
|
+
return writeJsonMarkdownArtifacts({
|
|
95
|
+
jsonPath: options.jsonPath,
|
|
96
|
+
markdownPath: options.markdownPath,
|
|
97
|
+
json: readiness,
|
|
98
|
+
markdown: renderColdImportReadinessMarkdown(readiness, options),
|
|
99
|
+
check: options.check,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function renderColdImportReadinessMarkdown(readiness, options = {}) {
|
|
104
|
+
return [
|
|
105
|
+
`# ${options.title ?? "Plugin Inspector Cold Import Readiness"}`,
|
|
106
|
+
"",
|
|
107
|
+
`Generated: ${readiness.generatedAt}`,
|
|
108
|
+
"",
|
|
109
|
+
"## Summary",
|
|
110
|
+
"",
|
|
111
|
+
markdownTable(
|
|
112
|
+
[
|
|
113
|
+
["Fixtures", readiness.summary.fixtureCount],
|
|
114
|
+
["Entrypoints", readiness.summary.entrypointCount],
|
|
115
|
+
["Ready", readiness.summary.readyCount],
|
|
116
|
+
["Blocked", readiness.summary.blockedCount],
|
|
117
|
+
["TypeScript loader required", readiness.summary.tsLoaderRequiredCount],
|
|
118
|
+
["Build required", readiness.summary.buildRequiredCount],
|
|
119
|
+
["Dependency install required", readiness.summary.dependencyInstallRequiredCount],
|
|
120
|
+
["SDK alias required", readiness.summary.sdkAliasRequiredCount],
|
|
121
|
+
],
|
|
122
|
+
["Metric", "Value"],
|
|
123
|
+
),
|
|
124
|
+
"",
|
|
125
|
+
"## Entrypoints",
|
|
126
|
+
"",
|
|
127
|
+
markdownTable(
|
|
128
|
+
readiness.fixtures.flatMap((fixture) =>
|
|
129
|
+
fixture.entrypoints.map((entrypoint) => [
|
|
130
|
+
fixture.id,
|
|
131
|
+
entrypoint.kind,
|
|
132
|
+
entrypoint.status,
|
|
133
|
+
entrypoint.path,
|
|
134
|
+
entrypoint.blockers.map((blocker) => blocker.code).join(", ") || "-",
|
|
135
|
+
entrypoint.assertions.join("; "),
|
|
136
|
+
]),
|
|
137
|
+
),
|
|
138
|
+
["Fixture", "Kind", "Status", "Path", "Blockers", "Assertions"],
|
|
139
|
+
),
|
|
140
|
+
].join("\n");
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function classifyEntrypointReadiness({ fixture, packageSummary, entrypoint, rootDir, sdkBlockers }) {
|
|
144
|
+
const blockers = [];
|
|
145
|
+
const resolvedPath = path.resolve(rootDir, entrypoint.relativePath);
|
|
146
|
+
const extension = path.extname(entrypoint.relativePath);
|
|
147
|
+
|
|
148
|
+
if (!entrypoint.exists) {
|
|
149
|
+
blockers.push({
|
|
150
|
+
code: entrypoint.requiresBuild ? "build-required" : "missing-entrypoint",
|
|
151
|
+
message: entrypoint.requiresBuild
|
|
152
|
+
? "entrypoint points at build output that is absent in the fixture checkout"
|
|
153
|
+
: "entrypoint path is missing in the fixture checkout",
|
|
154
|
+
evidence: entrypoint.relativePath,
|
|
155
|
+
});
|
|
156
|
+
} else if (extension === ".ts" || extension === ".tsx") {
|
|
157
|
+
blockers.push({
|
|
158
|
+
code: "ts-loader-required",
|
|
159
|
+
message: "entrypoint is TypeScript source and needs a loader or build step before Node cold import",
|
|
160
|
+
evidence: entrypoint.relativePath,
|
|
161
|
+
});
|
|
162
|
+
} else if (![".js", ".mjs", ".cjs"].includes(extension)) {
|
|
163
|
+
blockers.push({
|
|
164
|
+
code: "unknown-entrypoint-extension",
|
|
165
|
+
message: "entrypoint extension is not directly importable by the default Node runner",
|
|
166
|
+
evidence: entrypoint.relativePath,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (entrypoint.exists && existsSync(resolvedPath)) {
|
|
171
|
+
const source = readSourcePreviewSync(resolvedPath);
|
|
172
|
+
if (source && /\b(process\.env|spawn\(|execFile\(|exec\(|fetch\(|WebSocket\b)/.test(source)) {
|
|
173
|
+
blockers.push({
|
|
174
|
+
code: "top-level-side-effect-review",
|
|
175
|
+
message: "entrypoint source contains side-effect-prone tokens that cold import must sandbox or review",
|
|
176
|
+
evidence: entrypoint.relativePath,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const runtimeDependencies = unique([
|
|
182
|
+
...(packageSummary.dependencies ?? []),
|
|
183
|
+
...(packageSummary.peerDependencies ?? []),
|
|
184
|
+
...(packageSummary.optionalDependencies ?? []),
|
|
185
|
+
]);
|
|
186
|
+
if (entrypoint.exists && runtimeDependencies.length > 0) {
|
|
187
|
+
blockers.push({
|
|
188
|
+
code: "dependency-install-required",
|
|
189
|
+
message: "package declares runtime dependencies that must be installed before cold import",
|
|
190
|
+
evidence: runtimeDependencies.join(", "),
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
for (const sdkBlocker of sdkBlockers) {
|
|
195
|
+
blockers.push({
|
|
196
|
+
code: "sdk-alias-required",
|
|
197
|
+
message: "fixture imports an SDK alias missing from target OpenClaw package exports",
|
|
198
|
+
evidence: sdkBlocker,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
id: `cold-import.${entrypoint.kind}:${fixture.id}:${slugForArtifact(entrypoint.relativePath)}`,
|
|
204
|
+
fixture: fixture.id,
|
|
205
|
+
packagePath: packageSummary.path,
|
|
206
|
+
kind: entrypoint.kind,
|
|
207
|
+
specifier: entrypoint.specifier,
|
|
208
|
+
path: entrypoint.relativePath,
|
|
209
|
+
status: readinessStatus(blockers),
|
|
210
|
+
blockers,
|
|
211
|
+
assertions: coldImportAssertions(blockers),
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function readSourcePreviewSync(filePath) {
|
|
216
|
+
try {
|
|
217
|
+
return existsSync(filePath) ? readFileSync(filePath, "utf8").slice(0, 20000) : "";
|
|
218
|
+
} catch {
|
|
219
|
+
return "";
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function readinessStatus(blockers) {
|
|
224
|
+
if (blockers.length === 0) {
|
|
225
|
+
return "ready";
|
|
226
|
+
}
|
|
227
|
+
if (blockers.some((blocker) => blocker.code === "sdk-alias-required")) {
|
|
228
|
+
return "sdk-alias-required";
|
|
229
|
+
}
|
|
230
|
+
if (blockers.some((blocker) => blocker.code === "build-required")) {
|
|
231
|
+
return "build-required";
|
|
232
|
+
}
|
|
233
|
+
if (blockers.some((blocker) => blocker.code === "missing-entrypoint")) {
|
|
234
|
+
return "missing";
|
|
235
|
+
}
|
|
236
|
+
if (blockers.some((blocker) => blocker.code === "ts-loader-required")) {
|
|
237
|
+
return "ts-loader-required";
|
|
238
|
+
}
|
|
239
|
+
if (blockers.some((blocker) => blocker.code === "dependency-install-required")) {
|
|
240
|
+
return "dependency-install-required";
|
|
241
|
+
}
|
|
242
|
+
return "review-required";
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function coldImportAssertions(blockers) {
|
|
246
|
+
if (blockers.length === 0) {
|
|
247
|
+
return ["entrypoint can be imported by Node without fixture credentials", "registration capture shim receives plugin registrations"];
|
|
248
|
+
}
|
|
249
|
+
return blockers.map((blocker) => assertionForBlocker(blocker.code));
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function assertionForBlocker(code) {
|
|
253
|
+
const assertions = {
|
|
254
|
+
"build-required": "plugin build or source alias resolution runs before cold import",
|
|
255
|
+
"dependency-install-required": "fixture dependencies are installed in an isolated workspace before cold import",
|
|
256
|
+
"missing-entrypoint": "plugin package metadata points at an existing OpenClaw entrypoint",
|
|
257
|
+
"sdk-alias-required": "target OpenClaw exports the imported SDK alias or provides a migration shim",
|
|
258
|
+
"top-level-side-effect-review": "cold import sandbox blocks network/process side effects before register capture",
|
|
259
|
+
"ts-loader-required": "TypeScript source entrypoint is compiled or loaded before cold import",
|
|
260
|
+
"unknown-entrypoint-extension": "entrypoint extension has an explicit loader",
|
|
261
|
+
};
|
|
262
|
+
return assertions[code] ?? "cold import blocker has a documented mitigation";
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function unique(values) {
|
|
266
|
+
return [...new Set(values)];
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function markdownTable(rows, headers) {
|
|
270
|
+
return renderPaddedMarkdownTable(rows, headers);
|
|
271
|
+
}
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
import { renderPaddedMarkdownTable } from "./artifacts.js";
|
|
2
|
+
|
|
3
|
+
const defaultSeverityLabels = {
|
|
4
|
+
P0: "P0",
|
|
5
|
+
P1: "P1",
|
|
6
|
+
P2: "P2",
|
|
7
|
+
P3: "P3",
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export function renderCompatibilityMarkdownReport(report, options = {}) {
|
|
11
|
+
return [
|
|
12
|
+
`# ${options.title ?? "OpenClaw Plugin Compatibility Report"}`,
|
|
13
|
+
"",
|
|
14
|
+
`Generated: ${report.generatedAt}`,
|
|
15
|
+
`Status: ${report.status.toUpperCase()}`,
|
|
16
|
+
"",
|
|
17
|
+
"## Summary",
|
|
18
|
+
"",
|
|
19
|
+
markdownTable(
|
|
20
|
+
[
|
|
21
|
+
["Fixtures", report.summary.fixtureCount],
|
|
22
|
+
["High-priority fixtures", report.summary.highPriorityFixtures],
|
|
23
|
+
["Hard breakages", report.summary.breakageCount],
|
|
24
|
+
["Warnings", report.summary.warningCount],
|
|
25
|
+
["Compatibility suggestions", report.summary.suggestionCount],
|
|
26
|
+
["Issue findings", report.summary.issueCount],
|
|
27
|
+
["P0 issues", report.summary.p0IssueCount],
|
|
28
|
+
["P1 issues", report.summary.p1IssueCount],
|
|
29
|
+
["Live issues", report.summary.liveIssueCount],
|
|
30
|
+
["Live P0 issues", report.summary.liveP0IssueCount],
|
|
31
|
+
["Compat gaps", report.summary.compatGapCount],
|
|
32
|
+
["Deprecation warnings", report.summary.deprecationWarningCount],
|
|
33
|
+
["Inspector gaps", report.summary.inspectorGapCount],
|
|
34
|
+
["Upstream metadata", report.summary.upstreamIssueCount],
|
|
35
|
+
["Contract probes", report.summary.contractProbeCount],
|
|
36
|
+
["Decision rows", report.summary.decisionCount],
|
|
37
|
+
],
|
|
38
|
+
["Metric", "Value"],
|
|
39
|
+
),
|
|
40
|
+
"",
|
|
41
|
+
"## Triage Overview",
|
|
42
|
+
"",
|
|
43
|
+
triageOverview(report),
|
|
44
|
+
"",
|
|
45
|
+
"## P0 Live Issues",
|
|
46
|
+
"",
|
|
47
|
+
issuesTable(
|
|
48
|
+
report.issues.filter((issue) => issue.issueClass === "live-issue" && issue.severity === "P0"),
|
|
49
|
+
options,
|
|
50
|
+
),
|
|
51
|
+
"",
|
|
52
|
+
"## Live Issues",
|
|
53
|
+
"",
|
|
54
|
+
issuesTable(report.issues.filter((issue) => issue.issueClass === "live-issue"), options),
|
|
55
|
+
"",
|
|
56
|
+
"## Compat Gaps",
|
|
57
|
+
"",
|
|
58
|
+
issuesTable(report.issues.filter((issue) => issue.issueClass === "compat-gap"), options),
|
|
59
|
+
"",
|
|
60
|
+
"## Deprecation Warnings",
|
|
61
|
+
"",
|
|
62
|
+
issuesTable(report.issues.filter((issue) => issue.issueClass === "deprecation-warning"), options),
|
|
63
|
+
"",
|
|
64
|
+
"## Inspector Proof Gaps",
|
|
65
|
+
"",
|
|
66
|
+
issuesTable(report.issues.filter((issue) => issue.issueClass === "inspector-gap"), options),
|
|
67
|
+
"",
|
|
68
|
+
"## Upstream Metadata Issues",
|
|
69
|
+
"",
|
|
70
|
+
issuesTable(report.issues.filter((issue) => issue.issueClass === "upstream-metadata"), options),
|
|
71
|
+
"",
|
|
72
|
+
"## Hard Breakages",
|
|
73
|
+
"",
|
|
74
|
+
findingsTable(report.breakages),
|
|
75
|
+
"",
|
|
76
|
+
"## Target OpenClaw Compat Records",
|
|
77
|
+
"",
|
|
78
|
+
targetOpenClawTable(report.targetOpenClaw),
|
|
79
|
+
"",
|
|
80
|
+
"## Warnings",
|
|
81
|
+
"",
|
|
82
|
+
findingsTable(report.warnings),
|
|
83
|
+
"",
|
|
84
|
+
"## Suggestions To OpenClaw Compat Layer",
|
|
85
|
+
"",
|
|
86
|
+
findingsTable(report.suggestions),
|
|
87
|
+
"",
|
|
88
|
+
"## Issue Findings",
|
|
89
|
+
"",
|
|
90
|
+
issuesTable(report.issues, options),
|
|
91
|
+
"",
|
|
92
|
+
"## Contract Probe Backlog",
|
|
93
|
+
"",
|
|
94
|
+
contractProbesTable(report.contractProbes, options),
|
|
95
|
+
"",
|
|
96
|
+
"## Fixture Seam Inventory",
|
|
97
|
+
"",
|
|
98
|
+
markdownTable(
|
|
99
|
+
report.fixtures.map((fixture) => [
|
|
100
|
+
fixture.id,
|
|
101
|
+
fixture.priority,
|
|
102
|
+
fixture.seams.join(", "),
|
|
103
|
+
fixture.hooks.join(", ") || "-",
|
|
104
|
+
fixture.registrations.join(", ") || "-",
|
|
105
|
+
fixture.manifestContracts.join(", ") || "-",
|
|
106
|
+
]),
|
|
107
|
+
["Fixture", "Priority", "Seams", "Hooks", "Registrations", "Manifest contracts"],
|
|
108
|
+
),
|
|
109
|
+
"",
|
|
110
|
+
"## Decision Matrix",
|
|
111
|
+
"",
|
|
112
|
+
markdownTable(
|
|
113
|
+
report.decisions.map((decision) => [
|
|
114
|
+
decision.fixture,
|
|
115
|
+
decision.decision,
|
|
116
|
+
decision.seam,
|
|
117
|
+
decision.action,
|
|
118
|
+
decision.evidence,
|
|
119
|
+
]),
|
|
120
|
+
["Fixture", "Decision", "Seam", "Action", "Evidence"],
|
|
121
|
+
),
|
|
122
|
+
"",
|
|
123
|
+
"## Raw Logs",
|
|
124
|
+
"",
|
|
125
|
+
findingsTable(report.logs),
|
|
126
|
+
].join("\n");
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function renderCompatibilityIssuesReport(report, options = {}) {
|
|
130
|
+
return [
|
|
131
|
+
`# ${options.title ?? "OpenClaw Plugin Issue Findings"}`,
|
|
132
|
+
"",
|
|
133
|
+
`Generated: ${report.generatedAt}`,
|
|
134
|
+
`Status: ${report.status.toUpperCase()}`,
|
|
135
|
+
"",
|
|
136
|
+
"## Triage Summary",
|
|
137
|
+
"",
|
|
138
|
+
markdownTable(
|
|
139
|
+
[
|
|
140
|
+
["Issue findings", report.summary.issueCount],
|
|
141
|
+
[severityLabel("P0", options), report.summary.p0IssueCount],
|
|
142
|
+
[severityLabel("P1", options), report.summary.p1IssueCount],
|
|
143
|
+
["Live issues", report.summary.liveIssueCount],
|
|
144
|
+
["Live P0 issues", report.summary.liveP0IssueCount],
|
|
145
|
+
["Compat gaps", report.summary.compatGapCount],
|
|
146
|
+
["Deprecation warnings", report.summary.deprecationWarningCount],
|
|
147
|
+
["Inspector gaps", report.summary.inspectorGapCount],
|
|
148
|
+
["Upstream metadata", report.summary.upstreamIssueCount],
|
|
149
|
+
["Contract probes", report.summary.contractProbeCount],
|
|
150
|
+
],
|
|
151
|
+
["Metric", "Value"],
|
|
152
|
+
),
|
|
153
|
+
"",
|
|
154
|
+
"## Triage Overview",
|
|
155
|
+
"",
|
|
156
|
+
triageOverview(report),
|
|
157
|
+
"",
|
|
158
|
+
"## P0 Live Issues",
|
|
159
|
+
"",
|
|
160
|
+
issuesTable(
|
|
161
|
+
report.issues.filter((issue) => issue.issueClass === "live-issue" && issue.severity === "P0"),
|
|
162
|
+
options,
|
|
163
|
+
),
|
|
164
|
+
"",
|
|
165
|
+
"## Live Issues",
|
|
166
|
+
"",
|
|
167
|
+
issuesTable(report.issues.filter((issue) => issue.issueClass === "live-issue"), options),
|
|
168
|
+
"",
|
|
169
|
+
"## Compat Gaps",
|
|
170
|
+
"",
|
|
171
|
+
issuesTable(report.issues.filter((issue) => issue.issueClass === "compat-gap"), options),
|
|
172
|
+
"",
|
|
173
|
+
"## Deprecation Warnings",
|
|
174
|
+
"",
|
|
175
|
+
issuesTable(report.issues.filter((issue) => issue.issueClass === "deprecation-warning"), options),
|
|
176
|
+
"",
|
|
177
|
+
"## Inspector Proof Gaps",
|
|
178
|
+
"",
|
|
179
|
+
issuesTable(report.issues.filter((issue) => issue.issueClass === "inspector-gap"), options),
|
|
180
|
+
"",
|
|
181
|
+
"## Upstream Metadata Issues",
|
|
182
|
+
"",
|
|
183
|
+
issuesTable(report.issues.filter((issue) => issue.issueClass === "upstream-metadata"), options),
|
|
184
|
+
"",
|
|
185
|
+
"## Issues",
|
|
186
|
+
"",
|
|
187
|
+
issuesTable(report.issues, options),
|
|
188
|
+
"",
|
|
189
|
+
"## Contract Probe Backlog",
|
|
190
|
+
"",
|
|
191
|
+
contractProbesTable(report.contractProbes, options),
|
|
192
|
+
].join("\n");
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function findingsTable(findings) {
|
|
196
|
+
if (findings.length === 0) {
|
|
197
|
+
return "_none_";
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return markdownTable(
|
|
201
|
+
findings.map((finding) => [
|
|
202
|
+
finding.fixture,
|
|
203
|
+
finding.code,
|
|
204
|
+
finding.level,
|
|
205
|
+
finding.message,
|
|
206
|
+
(finding.evidence ?? []).join(", ") || "-",
|
|
207
|
+
finding.compatRecord ?? "-",
|
|
208
|
+
]),
|
|
209
|
+
["Fixture", "Code", "Level", "Message", "Evidence", "Compat record"],
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function issuesTable(issues, options) {
|
|
214
|
+
if (issues.length === 0) {
|
|
215
|
+
return "_none_";
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return issues.map((issue) => issueBlock(issue, options)).join("\n\n");
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function issueBlock(issue, options) {
|
|
222
|
+
return [
|
|
223
|
+
`- ${severityLabel(issue.severity, options)} **${issue.fixture}** \`${issue.issueClass}\` \`${issue.decision}\``,
|
|
224
|
+
` - **${issue.code}**: ${issue.title}`,
|
|
225
|
+
` - state: ${issueState(issue)}`,
|
|
226
|
+
" - evidence:",
|
|
227
|
+
...evidenceList(issue.evidence, options).map((item) => ` - ${item}`),
|
|
228
|
+
].join("\n");
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function issueState(issue) {
|
|
232
|
+
const flags = [
|
|
233
|
+
issue.status,
|
|
234
|
+
`compat:${issue.compatStatus ?? "none"}`,
|
|
235
|
+
issue.live ? "live" : null,
|
|
236
|
+
issue.deprecated ? "deprecated" : null,
|
|
237
|
+
].filter(Boolean);
|
|
238
|
+
return flags.join(" · ");
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function triageOverview(report) {
|
|
242
|
+
return markdownTable(
|
|
243
|
+
[
|
|
244
|
+
[
|
|
245
|
+
"live-issue",
|
|
246
|
+
report.summary.liveIssueCount,
|
|
247
|
+
report.summary.liveP0IssueCount,
|
|
248
|
+
"Potential runtime breakage in the target OpenClaw/plugin pair. P0 only when it is not a deprecated compat seam.",
|
|
249
|
+
],
|
|
250
|
+
[
|
|
251
|
+
"compat-gap",
|
|
252
|
+
report.summary.compatGapCount,
|
|
253
|
+
"-",
|
|
254
|
+
"Compatibility behavior is needed but missing from the target OpenClaw compat registry.",
|
|
255
|
+
],
|
|
256
|
+
[
|
|
257
|
+
"deprecation-warning",
|
|
258
|
+
report.summary.deprecationWarningCount,
|
|
259
|
+
"-",
|
|
260
|
+
"Plugin uses a supported but deprecated compatibility seam; keep it wired while migration exists.",
|
|
261
|
+
],
|
|
262
|
+
[
|
|
263
|
+
"inspector-gap",
|
|
264
|
+
report.summary.inspectorGapCount,
|
|
265
|
+
"-",
|
|
266
|
+
"Plugin Inspector needs stronger capture/probe evidence before making contract judgments.",
|
|
267
|
+
],
|
|
268
|
+
[
|
|
269
|
+
"upstream-metadata",
|
|
270
|
+
report.summary.upstreamIssueCount,
|
|
271
|
+
"-",
|
|
272
|
+
"Plugin package or manifest metadata should improve upstream; not a target OpenClaw live break by itself.",
|
|
273
|
+
],
|
|
274
|
+
[
|
|
275
|
+
"fixture-regression",
|
|
276
|
+
report.summary.fixtureRegressionCount,
|
|
277
|
+
"-",
|
|
278
|
+
"Fixture no longer exposes an expected seam; investigate fixture pin or scanner drift.",
|
|
279
|
+
],
|
|
280
|
+
],
|
|
281
|
+
["Class", "Count", "P0", "Meaning"],
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function contractProbesTable(probes, options) {
|
|
286
|
+
if (probes.length === 0) {
|
|
287
|
+
return "_none_";
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return probes.map((probe) => contractProbeBlock(probe, options)).join("\n\n");
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function contractProbeBlock(probe, options) {
|
|
294
|
+
return [
|
|
295
|
+
`- ${severityLabel(probe.priority, options)} **${probe.fixture}** \`${probe.target}\``,
|
|
296
|
+
` - contract: ${probe.contract}`,
|
|
297
|
+
` - id: \`${probe.id}\``,
|
|
298
|
+
" - evidence:",
|
|
299
|
+
...evidenceList(probe.evidence, options).map((item) => ` - ${item}`),
|
|
300
|
+
].join("\n");
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function targetOpenClawTable(targetOpenClaw = {}) {
|
|
304
|
+
const compatRecords = targetOpenClaw.compatRecords ?? [];
|
|
305
|
+
const recordPreview = compatRecords.length > 0 ? compatRecords.join(", ") : "-";
|
|
306
|
+
const statusCounts = Object.values(targetOpenClaw.compatRecordStatuses ?? {}).reduce((counts, status) => {
|
|
307
|
+
counts[status] = (counts[status] ?? 0) + 1;
|
|
308
|
+
return counts;
|
|
309
|
+
}, {});
|
|
310
|
+
return markdownTable(
|
|
311
|
+
[
|
|
312
|
+
["Configured path", targetOpenClaw.configuredPath ?? "-"],
|
|
313
|
+
["Status", targetOpenClaw.status],
|
|
314
|
+
["Compat registry", targetOpenClaw.compatRegistryPath ?? "-"],
|
|
315
|
+
["Compat records", targetOpenClaw.compatRecordCount ?? 0],
|
|
316
|
+
["Compat status counts", Object.entries(statusCounts).map(([status, count]) => `${status}:${count}`).join(", ") || "-"],
|
|
317
|
+
["Record ids", recordPreview],
|
|
318
|
+
["Hook registry", targetOpenClaw.hookTypesPath ?? "-"],
|
|
319
|
+
["Hook names", targetOpenClaw.hookNameCount ?? 0],
|
|
320
|
+
["API builder", targetOpenClaw.apiBuilderPath ?? "-"],
|
|
321
|
+
["API registrars", targetOpenClaw.apiRegistrarCount ?? 0],
|
|
322
|
+
["Captured registration", targetOpenClaw.capturedRegistrationPath ?? "-"],
|
|
323
|
+
["Captured registrars", targetOpenClaw.capturedRegistrarCount ?? 0],
|
|
324
|
+
["Package metadata", targetOpenClaw.packagePath ?? "-"],
|
|
325
|
+
["Plugin SDK exports", targetOpenClaw.sdkExportCount ?? 0],
|
|
326
|
+
["Manifest types", targetOpenClaw.manifestTypesPath ?? "-"],
|
|
327
|
+
["Manifest fields", targetOpenClaw.manifestFieldCount ?? 0],
|
|
328
|
+
["Manifest contract fields", targetOpenClaw.manifestContractFieldCount ?? 0],
|
|
329
|
+
],
|
|
330
|
+
["Metric", "Value"],
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function markdownTable(rows, headers) {
|
|
335
|
+
return renderPaddedMarkdownTable(
|
|
336
|
+
rows.map((row) => row.map((cell) => escapeCell(String(cell)))),
|
|
337
|
+
headers.map((header) => escapeCell(String(header))),
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function escapeCell(value) {
|
|
342
|
+
return String(value).replaceAll("|", "\\|").replaceAll("\n", " ");
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function severityLabel(severity, options) {
|
|
346
|
+
return { ...defaultSeverityLabels, ...options.severityLabels }[severity] ?? severity ?? "-";
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function evidenceList(evidence, options) {
|
|
350
|
+
const items = (evidence ?? []).filter(Boolean);
|
|
351
|
+
if (items.length === 0) {
|
|
352
|
+
return ["-"];
|
|
353
|
+
}
|
|
354
|
+
const formatEvidence = options.formatEvidence ?? ((item) => item);
|
|
355
|
+
return items.map((item) => formatEvidence(item));
|
|
356
|
+
}
|