@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,156 @@
|
|
|
1
|
+
export const contractProbeRules = {
|
|
2
|
+
"before-tool-call-probe": {
|
|
3
|
+
id: "hook.before_tool_call.terminal-block-approval",
|
|
4
|
+
contract: "Hook returns preserve terminal, block, and approval semantics.",
|
|
5
|
+
target: "hook-runner",
|
|
6
|
+
},
|
|
7
|
+
"channel-contract-probe": {
|
|
8
|
+
id: "channel.runtime.envelope-config-metadata",
|
|
9
|
+
contract: "Channel setup, message envelope, sender metadata, and config schema remain stable.",
|
|
10
|
+
target: "channel-runtime",
|
|
11
|
+
},
|
|
12
|
+
"conversation-access-hook": {
|
|
13
|
+
id: "hook.llm-observer.privacy-payload",
|
|
14
|
+
contract: "LLM observer hooks receive documented prompt/output fields with expected redaction behavior.",
|
|
15
|
+
target: "hook-runner",
|
|
16
|
+
},
|
|
17
|
+
"legacy-root-sdk-import": {
|
|
18
|
+
id: "sdk.import.root-barrel-cold-import",
|
|
19
|
+
contract: "Root plugin SDK barrel remains importable or has a machine-readable migration path.",
|
|
20
|
+
target: "sdk-alias",
|
|
21
|
+
},
|
|
22
|
+
"legacy-before-agent-start": {
|
|
23
|
+
id: "hook.compat.before-agent-start-migration",
|
|
24
|
+
contract: "Legacy before_agent_start remains wired until plugins migrate to before_model_resolve and before_prompt_build.",
|
|
25
|
+
target: "hook-runner",
|
|
26
|
+
},
|
|
27
|
+
"sdk-export-missing": {
|
|
28
|
+
id: "sdk.import.package-export-cold-import",
|
|
29
|
+
contract: "Every observed OpenClaw plugin SDK import remains exported by the target OpenClaw package.",
|
|
30
|
+
target: "sdk-alias",
|
|
31
|
+
},
|
|
32
|
+
"provider-auth-env-vars": {
|
|
33
|
+
id: "manifest.compat.provider-auth-env-vars",
|
|
34
|
+
contract: "Legacy provider auth env metadata continues to map into config/help surfaces.",
|
|
35
|
+
target: "manifest-loader",
|
|
36
|
+
},
|
|
37
|
+
"channel-env-vars": {
|
|
38
|
+
id: "manifest.compat.channel-env-vars",
|
|
39
|
+
contract: "Legacy channel env metadata continues to map into channel setup/help surfaces.",
|
|
40
|
+
target: "manifest-loader",
|
|
41
|
+
},
|
|
42
|
+
"manifest-unknown-contracts": {
|
|
43
|
+
id: "manifest.schema.contract-keys",
|
|
44
|
+
contract: "Manifest contract keys are represented in target OpenClaw PluginManifestContracts.",
|
|
45
|
+
target: "manifest-loader",
|
|
46
|
+
},
|
|
47
|
+
"manifest-unknown-fields": {
|
|
48
|
+
id: "manifest.schema.top-level-fields",
|
|
49
|
+
contract: "Manifest top-level fields are represented in target OpenClaw PluginManifest.",
|
|
50
|
+
target: "manifest-loader",
|
|
51
|
+
},
|
|
52
|
+
"registration-capture-gap": {
|
|
53
|
+
id: "api.capture.runtime-registrars",
|
|
54
|
+
contract: "External inspector capture records service, route, gateway, command, and interactive registrations.",
|
|
55
|
+
target: "inspector-capture-api",
|
|
56
|
+
},
|
|
57
|
+
"package-build-artifact-entrypoint": {
|
|
58
|
+
id: "package.entrypoint.build-before-cold-import",
|
|
59
|
+
contract: "Inspector can build or resolve source aliases before cold importing package entrypoints.",
|
|
60
|
+
target: "package-loader",
|
|
61
|
+
},
|
|
62
|
+
"package-dependency-install-required": {
|
|
63
|
+
id: "package.entrypoint.isolated-dependency-install",
|
|
64
|
+
contract: "Inspector installs package dependencies in an isolated workspace before cold import.",
|
|
65
|
+
target: "package-loader",
|
|
66
|
+
},
|
|
67
|
+
"package-entrypoint-missing": {
|
|
68
|
+
id: "package.entrypoint.exists",
|
|
69
|
+
contract: "OpenClaw package entrypoints resolve to files in the published or built plugin package.",
|
|
70
|
+
target: "package-loader",
|
|
71
|
+
},
|
|
72
|
+
"package-openclaw-entry-missing": {
|
|
73
|
+
id: "package.entrypoint.openclaw-metadata",
|
|
74
|
+
contract: "OpenClaw package metadata declares entrypoints for cold import and registration capture.",
|
|
75
|
+
target: "package-loader",
|
|
76
|
+
},
|
|
77
|
+
"package-openclaw-metadata-missing": {
|
|
78
|
+
id: "package.metadata.openclaw",
|
|
79
|
+
contract: "Plugins that register OpenClaw APIs declare OpenClaw install and entrypoint metadata.",
|
|
80
|
+
target: "package-loader",
|
|
81
|
+
},
|
|
82
|
+
"package-manifest-version-drift": {
|
|
83
|
+
id: "package.metadata.version-alignment",
|
|
84
|
+
contract: "Package and OpenClaw manifest versions stay aligned for release compatibility reporting.",
|
|
85
|
+
target: "package-loader",
|
|
86
|
+
},
|
|
87
|
+
"package-plugin-api-compat-missing": {
|
|
88
|
+
id: "package.compat.plugin-api-range",
|
|
89
|
+
contract: "Package metadata declares the OpenClaw plugin API range used by the plugin.",
|
|
90
|
+
target: "package-loader",
|
|
91
|
+
},
|
|
92
|
+
"package-typescript-source-entrypoint": {
|
|
93
|
+
id: "package.entrypoint.typescript-loader",
|
|
94
|
+
contract: "Inspector can compile or load TypeScript source entrypoints before registration capture.",
|
|
95
|
+
target: "package-loader",
|
|
96
|
+
},
|
|
97
|
+
"runtime-tool-capture": {
|
|
98
|
+
id: "tool.registration.schema-capture",
|
|
99
|
+
contract: "Registered runtime tools expose stable names, input schemas, and result metadata.",
|
|
100
|
+
target: "tool-runtime",
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
export function buildContractProbes({ warnings = [], suggestions = [], fixtures = [] }) {
|
|
105
|
+
const fixtureById = new Map(fixtures.map((fixture) => [fixture.id, fixture]));
|
|
106
|
+
const probes = [];
|
|
107
|
+
|
|
108
|
+
for (const finding of [...warnings, ...suggestions]) {
|
|
109
|
+
const rule = contractProbeRules[finding.code];
|
|
110
|
+
if (!rule) {
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
probes.push({
|
|
114
|
+
id: `${rule.id}:${finding.fixture}`,
|
|
115
|
+
fixture: finding.fixture,
|
|
116
|
+
priority: probePriority(finding.code, fixtureById.get(finding.fixture)?.priority),
|
|
117
|
+
target: rule.target,
|
|
118
|
+
contract: rule.contract,
|
|
119
|
+
evidence: finding.evidence ?? [],
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return dedupeBy(probes, (probe) => probe.id).sort(
|
|
124
|
+
(left, right) => priorityRank(left.priority) - priorityRank(right.priority) || left.id.localeCompare(right.id),
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function probePriority(code, fixturePriority) {
|
|
129
|
+
if (
|
|
130
|
+
[
|
|
131
|
+
"before-tool-call-probe",
|
|
132
|
+
"conversation-access-hook",
|
|
133
|
+
"missing-compat-record",
|
|
134
|
+
"registration-capture-gap",
|
|
135
|
+
"sdk-export-missing",
|
|
136
|
+
].includes(code)
|
|
137
|
+
) {
|
|
138
|
+
return "P1";
|
|
139
|
+
}
|
|
140
|
+
if (fixturePriority === "high") {
|
|
141
|
+
return "P2";
|
|
142
|
+
}
|
|
143
|
+
return "P3";
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function dedupeBy(values, keyForValue) {
|
|
147
|
+
const output = new Map();
|
|
148
|
+
for (const value of values) {
|
|
149
|
+
output.set(keyForValue(value), value);
|
|
150
|
+
}
|
|
151
|
+
return [...output.values()];
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function priorityRank(priority) {
|
|
155
|
+
return { P0: 0, P1: 1, P2: 2, P3: 3 }[priority] ?? 4;
|
|
156
|
+
}
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { renderPaddedMarkdownTable, writeJsonMarkdownArtifacts } from "./artifacts.js";
|
|
5
|
+
import { resolveFromRoot, toRepoPath } from "./path-utils.js";
|
|
6
|
+
|
|
7
|
+
export const defaultExecutionResultsOptions = {
|
|
8
|
+
generatedAt: "deterministic",
|
|
9
|
+
markdownPath: "reports/plugin-execution-results.md",
|
|
10
|
+
reportTitle: "Plugin Execution Results",
|
|
11
|
+
resultsDir: ".plugin-inspector/results",
|
|
12
|
+
jsonPath: "reports/plugin-execution-results.json",
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export async function buildExecutionResultsReport(options = {}) {
|
|
16
|
+
const rootDir = path.resolve(options.rootDir ?? process.cwd());
|
|
17
|
+
const resultsDir = resolveFromRoot(rootDir, options.resultsDir ?? defaultExecutionResultsOptions.resultsDir);
|
|
18
|
+
const artifacts = existsSync(resultsDir) ? await readArtifacts(resultsDir, { rootDir }) : [];
|
|
19
|
+
const syntheticArtifacts = artifacts.filter((artifact) => artifact.kind === "synthetic");
|
|
20
|
+
const captureArtifacts = artifacts.filter((artifact) => artifact.kind === "capture");
|
|
21
|
+
const auditArtifacts = artifacts.filter((artifact) => artifact.kind === "audit");
|
|
22
|
+
const profileArtifacts = artifacts.filter((artifact) => artifact.kind === "profile");
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
generatedAt: options.generatedAt ?? defaultExecutionResultsOptions.generatedAt,
|
|
26
|
+
resultsDir: repoRelative(resultsDir, { rootDir }),
|
|
27
|
+
summary: {
|
|
28
|
+
artifactCount: artifacts.length,
|
|
29
|
+
captureArtifactCount: captureArtifacts.length,
|
|
30
|
+
syntheticArtifactCount: syntheticArtifacts.length,
|
|
31
|
+
auditArtifactCount: auditArtifacts.length,
|
|
32
|
+
profileArtifactCount: profileArtifacts.length,
|
|
33
|
+
capturedRegistrationCount: captureArtifacts.reduce(
|
|
34
|
+
(sum, artifact) => sum + (artifact.capturedCount ?? 0),
|
|
35
|
+
0,
|
|
36
|
+
),
|
|
37
|
+
auditFindingCount: auditArtifacts.reduce((sum, artifact) => sum + artifact.findingCount, 0),
|
|
38
|
+
executionWallMs: profileArtifacts.reduce((sum, artifact) => sum + (artifact.summary?.totalWallMs ?? 0), 0),
|
|
39
|
+
maxPeakRssMb: Math.max(0, ...profileArtifacts.map((artifact) => artifact.summary?.maxPeakRssMb ?? 0)),
|
|
40
|
+
maxCpuMsEstimate: Math.max(0, ...profileArtifacts.map((artifact) => artifact.summary?.maxCpuMsEstimate ?? 0)),
|
|
41
|
+
passCount: syntheticArtifacts.reduce((sum, artifact) => sum + (artifact.summary?.passCount ?? 0), 0),
|
|
42
|
+
failCount: syntheticArtifacts.reduce((sum, artifact) => sum + (artifact.summary?.failCount ?? 0), 0),
|
|
43
|
+
blockedCount: syntheticArtifacts.reduce((sum, artifact) => sum + (artifact.summary?.blockedCount ?? 0), 0),
|
|
44
|
+
},
|
|
45
|
+
artifacts,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function writeExecutionResultsReport(report, options = {}) {
|
|
50
|
+
const rootDir = path.resolve(options.rootDir ?? process.cwd());
|
|
51
|
+
const jsonPath = resolveFromRoot(rootDir, options.jsonPath ?? defaultExecutionResultsOptions.jsonPath);
|
|
52
|
+
const markdownPath = resolveFromRoot(rootDir, options.markdownPath ?? defaultExecutionResultsOptions.markdownPath);
|
|
53
|
+
return writeJsonMarkdownArtifacts({
|
|
54
|
+
jsonPath,
|
|
55
|
+
markdownPath,
|
|
56
|
+
json: report,
|
|
57
|
+
markdown: renderExecutionResultsMarkdown(report, options),
|
|
58
|
+
check: options.check,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function renderExecutionResultsMarkdown(report, options = {}) {
|
|
63
|
+
const title = options.title ?? options.reportTitle ?? defaultExecutionResultsOptions.reportTitle;
|
|
64
|
+
return [
|
|
65
|
+
`# ${title}`,
|
|
66
|
+
"",
|
|
67
|
+
`Generated: ${report.generatedAt}`,
|
|
68
|
+
`Results dir: ${report.resultsDir}`,
|
|
69
|
+
"",
|
|
70
|
+
"## Summary",
|
|
71
|
+
"",
|
|
72
|
+
markdownTable(
|
|
73
|
+
[
|
|
74
|
+
["Artifacts", report.summary.artifactCount],
|
|
75
|
+
["Capture artifacts", report.summary.captureArtifactCount],
|
|
76
|
+
["Synthetic artifacts", report.summary.syntheticArtifactCount],
|
|
77
|
+
["Audit artifacts", report.summary.auditArtifactCount],
|
|
78
|
+
["Profile artifacts", report.summary.profileArtifactCount],
|
|
79
|
+
["Captured registrations/hooks", report.summary.capturedRegistrationCount],
|
|
80
|
+
["Audit findings", report.summary.auditFindingCount],
|
|
81
|
+
["Execution wall", `${report.summary.executionWallMs} ms`],
|
|
82
|
+
["Max peak RSS", `${report.summary.maxPeakRssMb} MB`],
|
|
83
|
+
["Max CPU estimate", `${report.summary.maxCpuMsEstimate} ms`],
|
|
84
|
+
["Pass", report.summary.passCount],
|
|
85
|
+
["Fail", report.summary.failCount],
|
|
86
|
+
["Blocked", report.summary.blockedCount],
|
|
87
|
+
],
|
|
88
|
+
["Metric", "Value"],
|
|
89
|
+
),
|
|
90
|
+
"",
|
|
91
|
+
"## Artifacts",
|
|
92
|
+
"",
|
|
93
|
+
markdownTable(
|
|
94
|
+
report.artifacts.map((artifact) => [
|
|
95
|
+
artifact.fixture,
|
|
96
|
+
artifact.kind,
|
|
97
|
+
artifact.status,
|
|
98
|
+
artifact.entrypoint,
|
|
99
|
+
summarizeArtifactResult(artifact),
|
|
100
|
+
artifact.artifactPath,
|
|
101
|
+
]),
|
|
102
|
+
["Fixture", "Kind", "Status", "Entrypoint", "Result", "Artifact"],
|
|
103
|
+
),
|
|
104
|
+
"",
|
|
105
|
+
"## Blocked Synthetic Probes",
|
|
106
|
+
"",
|
|
107
|
+
markdownTable(
|
|
108
|
+
report.artifacts.flatMap((artifact) =>
|
|
109
|
+
(artifact.blocked ?? []).map((item) => [
|
|
110
|
+
artifact.fixture,
|
|
111
|
+
item.kind,
|
|
112
|
+
item.seam,
|
|
113
|
+
item.label,
|
|
114
|
+
item.reason,
|
|
115
|
+
artifact.artifactPath,
|
|
116
|
+
]),
|
|
117
|
+
),
|
|
118
|
+
["Fixture", "Kind", "Seam", "Label", "Reason", "Artifact"],
|
|
119
|
+
),
|
|
120
|
+
"",
|
|
121
|
+
"## Failed Synthetic Probes",
|
|
122
|
+
"",
|
|
123
|
+
markdownTable(
|
|
124
|
+
report.artifacts.flatMap((artifact) =>
|
|
125
|
+
(artifact.failures ?? []).map((item) => [
|
|
126
|
+
artifact.fixture,
|
|
127
|
+
item.kind,
|
|
128
|
+
item.seam,
|
|
129
|
+
item.label,
|
|
130
|
+
item.error,
|
|
131
|
+
artifact.artifactPath,
|
|
132
|
+
]),
|
|
133
|
+
),
|
|
134
|
+
["Fixture", "Kind", "Seam", "Label", "Error", "Artifact"],
|
|
135
|
+
),
|
|
136
|
+
"",
|
|
137
|
+
"## Dependency Audit Artifacts",
|
|
138
|
+
"",
|
|
139
|
+
markdownTable(
|
|
140
|
+
report.artifacts
|
|
141
|
+
.filter((artifact) => artifact.kind === "audit")
|
|
142
|
+
.map((artifact) => [
|
|
143
|
+
artifact.fixture,
|
|
144
|
+
artifact.findingCount,
|
|
145
|
+
artifact.vulnerabilities ? JSON.stringify(artifact.vulnerabilities) : "-",
|
|
146
|
+
artifact.artifactPath,
|
|
147
|
+
]),
|
|
148
|
+
["Fixture", "Findings", "Vulnerabilities", "Artifact"],
|
|
149
|
+
),
|
|
150
|
+
"",
|
|
151
|
+
"## Execution Profiles",
|
|
152
|
+
"",
|
|
153
|
+
markdownTable(
|
|
154
|
+
report.artifacts.flatMap((artifact) =>
|
|
155
|
+
(artifact.slowestSteps ?? []).map((step) => [
|
|
156
|
+
artifact.fixture,
|
|
157
|
+
step.kind,
|
|
158
|
+
`${step.wallMs} ms`,
|
|
159
|
+
`${step.peakRssMb} MB`,
|
|
160
|
+
`${step.cpuMsEstimate} ms`,
|
|
161
|
+
step.command,
|
|
162
|
+
]),
|
|
163
|
+
),
|
|
164
|
+
["Fixture", "Step", "Wall", "Peak RSS", "CPU Estimate", "Command"],
|
|
165
|
+
),
|
|
166
|
+
].join("\n");
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async function readArtifacts(resultsDir, options) {
|
|
170
|
+
const paths = await listJsonFiles(resultsDir);
|
|
171
|
+
const artifacts = [];
|
|
172
|
+
for (const artifactPath of paths) {
|
|
173
|
+
const parsed = JSON.parse(await readFile(artifactPath, "utf8"));
|
|
174
|
+
const relativePath = repoRelative(artifactPath, options);
|
|
175
|
+
artifacts.push(summarizeArtifact({ artifactPath: relativePath, parsed, rootDir: options.rootDir }));
|
|
176
|
+
}
|
|
177
|
+
return artifacts.sort((left, right) => left.artifactPath.localeCompare(right.artifactPath));
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async function listJsonFiles(dir) {
|
|
181
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
182
|
+
const files = [];
|
|
183
|
+
for (const entry of entries) {
|
|
184
|
+
const entryPath = path.join(dir, entry.name);
|
|
185
|
+
if (entry.isDirectory()) {
|
|
186
|
+
files.push(...(await listJsonFiles(entryPath)));
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
if (entry.isFile() && entry.name.endsWith(".json")) {
|
|
190
|
+
files.push(entryPath);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return files;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function summarizeArtifact({ artifactPath, parsed, rootDir }) {
|
|
197
|
+
const normalizedArtifactPath = toRepoPath(artifactPath);
|
|
198
|
+
const kind = normalizedArtifactPath.endsWith(".synthetic.json")
|
|
199
|
+
? "synthetic"
|
|
200
|
+
: normalizedArtifactPath.endsWith("package-audit.json")
|
|
201
|
+
? "audit"
|
|
202
|
+
: normalizedArtifactPath.endsWith("execution-profile.json")
|
|
203
|
+
? "profile"
|
|
204
|
+
: "capture";
|
|
205
|
+
const fixture = normalizedArtifactPath.split("/").at(-2) ?? "unknown";
|
|
206
|
+
if (kind === "synthetic") {
|
|
207
|
+
return {
|
|
208
|
+
artifactPath: normalizedArtifactPath,
|
|
209
|
+
fixture,
|
|
210
|
+
kind,
|
|
211
|
+
entrypoint: scrubPath(parsed.entrypoint, { rootDir }),
|
|
212
|
+
status: parsed.status,
|
|
213
|
+
summary: parsed.summary,
|
|
214
|
+
failures: (parsed.results ?? []).filter((result) => result.status === "fail"),
|
|
215
|
+
blocked: (parsed.results ?? []).filter((result) => result.status === "blocked"),
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
if (kind === "audit") {
|
|
219
|
+
return {
|
|
220
|
+
artifactPath: normalizedArtifactPath,
|
|
221
|
+
fixture,
|
|
222
|
+
kind,
|
|
223
|
+
entrypoint: "-",
|
|
224
|
+
status: "warning",
|
|
225
|
+
findingCount: auditFindingCount(parsed),
|
|
226
|
+
vulnerabilities: parsed.metadata?.vulnerabilities ?? null,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
if (kind === "profile") {
|
|
230
|
+
return {
|
|
231
|
+
artifactPath: normalizedArtifactPath,
|
|
232
|
+
fixture,
|
|
233
|
+
kind,
|
|
234
|
+
entrypoint: "-",
|
|
235
|
+
status: parsed.summary?.failCount > 0 ? "fail" : "pass",
|
|
236
|
+
summary: parsed.summary,
|
|
237
|
+
slowestSteps: [...(parsed.steps ?? [])].sort((left, right) => right.wallMs - left.wallMs).slice(0, 5),
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
return {
|
|
241
|
+
artifactPath: normalizedArtifactPath,
|
|
242
|
+
fixture,
|
|
243
|
+
kind,
|
|
244
|
+
entrypoint: scrubPath(parsed.entrypoint, { rootDir }),
|
|
245
|
+
status: parsed.status,
|
|
246
|
+
capturedCount: parsed.captured?.length ?? 0,
|
|
247
|
+
captured: (parsed.captured ?? []).map((item) => `${item.kind}:${item.name}`),
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function summarizeArtifactResult(artifact) {
|
|
252
|
+
if (artifact.kind === "audit") {
|
|
253
|
+
return `${artifact.findingCount} audit findings`;
|
|
254
|
+
}
|
|
255
|
+
if (artifact.kind === "profile") {
|
|
256
|
+
return `${artifact.summary?.stepCount ?? 0} steps / ${artifact.summary?.totalWallMs ?? 0} ms / ${artifact.summary?.maxPeakRssMb ?? 0} MB`;
|
|
257
|
+
}
|
|
258
|
+
if (artifact.summary) {
|
|
259
|
+
return `${artifact.summary.passCount} pass / ${artifact.summary.failCount} fail / ${artifact.summary.blockedCount} blocked`;
|
|
260
|
+
}
|
|
261
|
+
return `${artifact.capturedCount} captured`;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function auditFindingCount(parsed) {
|
|
265
|
+
const vulnerabilities = parsed.metadata?.vulnerabilities;
|
|
266
|
+
if (vulnerabilities && typeof vulnerabilities === "object") {
|
|
267
|
+
const severityTotal = Object.entries(vulnerabilities)
|
|
268
|
+
.filter(([key]) => key !== "total")
|
|
269
|
+
.reduce((sum, [, value]) => sum + (Number(value) || 0), 0);
|
|
270
|
+
return severityTotal || Number(vulnerabilities.total) || 0;
|
|
271
|
+
}
|
|
272
|
+
if (Array.isArray(parsed.vulnerabilities)) {
|
|
273
|
+
return parsed.vulnerabilities.length;
|
|
274
|
+
}
|
|
275
|
+
if (parsed.vulnerabilities && typeof parsed.vulnerabilities === "object") {
|
|
276
|
+
return Object.keys(parsed.vulnerabilities).length;
|
|
277
|
+
}
|
|
278
|
+
return 0;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function scrubPath(value, options) {
|
|
282
|
+
return typeof value === "string" ? repoRelative(value, options) : value;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function repoRelative(value, options = {}) {
|
|
286
|
+
const rootDir = path.resolve(options.rootDir ?? process.cwd());
|
|
287
|
+
const absolute = path.resolve(value);
|
|
288
|
+
const relative = path.relative(rootDir, absolute);
|
|
289
|
+
if (relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative))) {
|
|
290
|
+
return toRepoPath(relative || ".");
|
|
291
|
+
}
|
|
292
|
+
return toRepoPath(value);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function markdownTable(rows, headers) {
|
|
296
|
+
return renderPaddedMarkdownTable(rows, headers);
|
|
297
|
+
}
|