@openclaw/plugin-inspector 0.0.0 → 0.1.1

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.
@@ -0,0 +1,115 @@
1
+ export function createCaptureApi(options = {}) {
2
+ const captured = [];
3
+ const retained = [];
4
+ const knownRegistrars = new Set(options.knownRegistrars ?? []);
5
+ const retainHandlers = options.retainHandlers === true;
6
+
7
+ const api = new Proxy(
8
+ {
9
+ config: options.config ?? {},
10
+ logger: options.logger ?? console,
11
+ pluginConfig: options.pluginConfig ?? {},
12
+ runtime: options.runtime ?? {},
13
+ on(name, handler) {
14
+ const captureIndex =
15
+ captured.push({
16
+ kind: "hook",
17
+ name,
18
+ handlerType: typeof handler,
19
+ arguments: summarizeArguments([name, handler]),
20
+ }) - 1;
21
+ if (retainHandlers) {
22
+ retained.push({
23
+ kind: "hook",
24
+ name,
25
+ handler,
26
+ captureIndex,
27
+ });
28
+ }
29
+ return api;
30
+ },
31
+ },
32
+ {
33
+ get(target, property) {
34
+ if (property === "getCapturedContracts") {
35
+ return () => captured.map((entry) => ({ ...entry }));
36
+ }
37
+ if (property === "getRetainedContracts") {
38
+ return () => retained.map((entry) => ({ ...entry }));
39
+ }
40
+ if (property in target) {
41
+ return target[property];
42
+ }
43
+ if (typeof property === "string" && isRegistrarProperty(property)) {
44
+ return (...args) => {
45
+ const captureIndex =
46
+ captured.push({
47
+ kind: "registration",
48
+ name: property,
49
+ known: knownRegistrars.size === 0 ? null : knownRegistrars.has(property),
50
+ arguments: summarizeArguments(args),
51
+ }) - 1;
52
+ if (retainHandlers) {
53
+ retained.push({
54
+ kind: "registration",
55
+ name: property,
56
+ arguments: args,
57
+ captureIndex,
58
+ });
59
+ }
60
+ return registrationReturnValue(property, args);
61
+ };
62
+ }
63
+ return undefined;
64
+ },
65
+ },
66
+ );
67
+
68
+ return api;
69
+ }
70
+
71
+ function isRegistrarProperty(property) {
72
+ return property.startsWith("register") || property.startsWith("define");
73
+ }
74
+
75
+ function registrationReturnValue(name, args) {
76
+ if (name === "registerService") {
77
+ return {
78
+ name: objectName(args[0]),
79
+ start: async () => undefined,
80
+ stop: async () => undefined,
81
+ };
82
+ }
83
+ return objectName(args[0]) ?? undefined;
84
+ }
85
+
86
+ function summarizeArguments(args) {
87
+ return args.map((arg) => summarizeValue(arg));
88
+ }
89
+
90
+ function summarizeValue(value) {
91
+ if (typeof value === "function") {
92
+ return { type: "function" };
93
+ }
94
+ if (Array.isArray(value)) {
95
+ return { type: "array", length: value.length };
96
+ }
97
+ if (value && typeof value === "object") {
98
+ return {
99
+ type: "object",
100
+ keys: Object.keys(value).sort(),
101
+ name: objectName(value),
102
+ };
103
+ }
104
+ return { type: typeof value, value };
105
+ }
106
+
107
+ function objectName(value) {
108
+ if (!value || typeof value !== "object") {
109
+ return null;
110
+ }
111
+ if (typeof value.name === "string") {
112
+ return value.name;
113
+ }
114
+ return typeof value.id === "string" ? value.id : null;
115
+ }
@@ -0,0 +1,259 @@
1
+ import path from "node:path";
2
+ import { renderPaddedMarkdownTable, writeJsonMarkdownArtifacts } from "./artifacts.js";
3
+ import { resolveFromRoot } from "./path-utils.js";
4
+
5
+ export const defaultCiPolicyReportOptions = {
6
+ generatedAt: "deterministic",
7
+ jsonPath: "reports/plugin-inspector-ci-policy.json",
8
+ markdownPath: "reports/plugin-inspector-ci-policy.md",
9
+ reportTitle: "Plugin Inspector CI Policy",
10
+ };
11
+
12
+ export function buildCiPolicyReport(options = {}) {
13
+ const policy = options.policy;
14
+ validateCiPolicy(policy);
15
+
16
+ const checks = [
17
+ ...compatibilityChecks(options.compatibilityReport, { strict: options.strict }),
18
+ ...refDiffChecks(options.refDiff, { strict: options.strict }),
19
+ ...executionChecks(options.executionResults, policy, { strict: options.strict }),
20
+ ].sort((left, right) => actionRank(left.action) - actionRank(right.action) || left.id.localeCompare(right.id));
21
+
22
+ return {
23
+ generatedAt: options.generatedAt ?? defaultCiPolicyReportOptions.generatedAt,
24
+ status: checks.some((check) => check.action === "fail") ? "fail" : "pass",
25
+ strict: Boolean(options.strict),
26
+ policy: {
27
+ allowedBlocked: policy.allowedBlocked.length,
28
+ expectedWarnings: policy.expectedWarnings.length,
29
+ fixtureSets: Object.keys(policy.fixtureSets).sort(),
30
+ thresholds: policy.thresholds,
31
+ },
32
+ summary: {
33
+ checkCount: checks.length,
34
+ failCount: checks.filter((check) => check.action === "fail").length,
35
+ warnCount: checks.filter((check) => check.action === "warn").length,
36
+ passCount: checks.filter((check) => check.action === "pass").length,
37
+ },
38
+ checks,
39
+ };
40
+ }
41
+
42
+ export function validateCiPolicy(policy) {
43
+ const errors = [];
44
+ if (policy?.version !== 1) {
45
+ errors.push("ci policy version must be 1");
46
+ }
47
+ for (const key of ["allowedBlocked", "expectedWarnings"]) {
48
+ if (!Array.isArray(policy?.[key])) {
49
+ errors.push(`ci policy ${key} must be an array`);
50
+ }
51
+ }
52
+ if (!policy?.thresholds || typeof policy.thresholds !== "object") {
53
+ errors.push("ci policy thresholds are required");
54
+ }
55
+ if (!policy?.fixtureSets || typeof policy.fixtureSets !== "object") {
56
+ errors.push("ci policy fixtureSets are required");
57
+ }
58
+ if (errors.length > 0) {
59
+ throw new Error(errors.join("\n"));
60
+ }
61
+ }
62
+
63
+ export function validateCiPolicyReport(report) {
64
+ return report.checks
65
+ .filter((check) => check.action === "fail")
66
+ .map((check) => `${check.id}: ${check.message}: ${check.evidence.join(", ")}`);
67
+ }
68
+
69
+ export async function writeCiPolicyReport(report, options = {}) {
70
+ const rootDir = path.resolve(options.rootDir ?? process.cwd());
71
+ const jsonPath = resolveFromRoot(rootDir, options.jsonPath ?? defaultCiPolicyReportOptions.jsonPath);
72
+ const markdownPath = resolveFromRoot(rootDir, options.markdownPath ?? defaultCiPolicyReportOptions.markdownPath);
73
+ return writeJsonMarkdownArtifacts({
74
+ jsonPath,
75
+ markdownPath,
76
+ json: report,
77
+ markdown: renderCiPolicyMarkdown(report, options),
78
+ check: options.check,
79
+ });
80
+ }
81
+
82
+ export function renderCiPolicyMarkdown(report, options = {}) {
83
+ const title = options.title ?? options.reportTitle ?? defaultCiPolicyReportOptions.reportTitle;
84
+ return [
85
+ `# ${title}`,
86
+ "",
87
+ `Generated: ${report.generatedAt}`,
88
+ `Status: ${report.status.toUpperCase()}`,
89
+ `Strict: ${report.strict}`,
90
+ "",
91
+ "## Summary",
92
+ "",
93
+ markdownTable(
94
+ [
95
+ ["Checks", report.summary.checkCount],
96
+ ["Fail", report.summary.failCount],
97
+ ["Warn", report.summary.warnCount],
98
+ ["Pass", report.summary.passCount],
99
+ ["Allowed blocked rules", report.policy.allowedBlocked],
100
+ ["Expected warning rules", report.policy.expectedWarnings],
101
+ ["Fixture sets", report.policy.fixtureSets.join(", ")],
102
+ ],
103
+ ["Metric", "Value"],
104
+ ),
105
+ "",
106
+ "## Checks",
107
+ "",
108
+ markdownTable(
109
+ report.checks.map((check) => [
110
+ check.action,
111
+ check.id,
112
+ check.message,
113
+ check.evidence.join(", ") || "-",
114
+ ]),
115
+ ["Action", "ID", "Message", "Evidence"],
116
+ ),
117
+ ].join("\n");
118
+ }
119
+
120
+ function compatibilityChecks(report, options) {
121
+ const checks = [];
122
+ if (!report) {
123
+ checks.push({
124
+ id: "compatibility-report.missing",
125
+ action: "fail",
126
+ message: "compatibility report is missing",
127
+ evidence: [],
128
+ });
129
+ return checks;
130
+ }
131
+ checks.push({
132
+ id: "compatibility-report.breakages",
133
+ action: report.summary.breakageCount > 0 ? "fail" : "pass",
134
+ message: `${report.summary.breakageCount} hard breakages`,
135
+ evidence: (report.breakages ?? []).map((finding) => `${finding.fixture}:${finding.code}`),
136
+ });
137
+ checks.push({
138
+ id: "compatibility-report.p1-issues",
139
+ action: "pass",
140
+ message: `${report.summary.p1IssueCount} P1 issues tracked`,
141
+ evidence: (report.issues ?? [])
142
+ .filter((issue) => issue.severity === "P1")
143
+ .map((issue) => `${issue.fixture}:${issue.code}`),
144
+ });
145
+ const issues = report.issues ?? [];
146
+ const liveP0Issues = issues.filter((issue) => issue.issueClass === "live-issue" && issue.severity === "P0");
147
+ const deprecationWarnings = issues.filter((issue) => issue.issueClass === "deprecation-warning");
148
+ const inspectorGaps = issues.filter((issue) => issue.issueClass === "inspector-gap");
149
+ checks.push({
150
+ id: "compatibility-report.live-p0-issues",
151
+ action: liveP0Issues.length > 0 ? (options.strict ? "fail" : "warn") : "pass",
152
+ message: `${liveP0Issues.length} live P0 issues tracked`,
153
+ evidence: liveP0Issues.map((issue) => `${issue.fixture}:${issue.code}:${issue.compatStatus ?? "none"}`),
154
+ });
155
+ checks.push({
156
+ id: "compatibility-report.deprecation-warnings",
157
+ action: "pass",
158
+ message: `${deprecationWarnings.length} deprecated compat seams tracked`,
159
+ evidence: deprecationWarnings.map((issue) => `${issue.fixture}:${issue.code}`),
160
+ });
161
+ checks.push({
162
+ id: "compatibility-report.inspector-gaps",
163
+ action: "pass",
164
+ message: `${inspectorGaps.length} inspector proof gaps tracked`,
165
+ evidence: inspectorGaps.map((issue) => `${issue.fixture}:${issue.code}`),
166
+ });
167
+ return checks;
168
+ }
169
+
170
+ function refDiffChecks(refDiff, options) {
171
+ if (!refDiff) {
172
+ return [
173
+ {
174
+ id: "ref-diff.not-run",
175
+ action: "pass",
176
+ message: "ref diff artifact was not present for this CI mode",
177
+ evidence: [],
178
+ },
179
+ ];
180
+ }
181
+
182
+ return (refDiff.regressions ?? []).map((regression) => ({
183
+ id: `ref-diff.${regression.code}`,
184
+ action: regression.action === "fail" || (options.strict && regression.action === "warn") ? "fail" : "warn",
185
+ message: regression.message,
186
+ evidence: regression.evidence ?? [],
187
+ }));
188
+ }
189
+
190
+ function executionChecks(executionResults, policy, options) {
191
+ if (!executionResults) {
192
+ return [
193
+ {
194
+ id: "execution-results.not-run",
195
+ action: "pass",
196
+ message: "isolated execution artifact was not present for this CI mode",
197
+ evidence: [],
198
+ },
199
+ ];
200
+ }
201
+
202
+ const checks = [
203
+ {
204
+ id: "execution-results.failures",
205
+ action: executionResults.summary.failCount > 0 ? "fail" : "pass",
206
+ message: `${executionResults.summary.failCount} failed synthetic probes`,
207
+ evidence: failedExecutionEvidence(executionResults),
208
+ },
209
+ {
210
+ id: "execution-results.audit-findings",
211
+ action: executionResults.summary.auditFindingCount > 0 ? "warn" : "pass",
212
+ message: `${executionResults.summary.auditFindingCount ?? 0} package audit findings`,
213
+ evidence: executionResults.artifacts
214
+ .filter((artifact) => artifact.kind === "audit" && artifact.findingCount > 0)
215
+ .map((artifact) => `${artifact.fixture}:${artifact.findingCount}`),
216
+ },
217
+ ];
218
+
219
+ const blocked = executionResults.artifacts.flatMap((artifact) =>
220
+ (artifact.blocked ?? []).map((item) => ({ artifact, item })),
221
+ );
222
+ for (const blockedItem of blocked) {
223
+ const expectedWarning = findPolicyMatch(policy.expectedWarnings, blockedItem.item);
224
+ const allowedBlocked = findPolicyMatch(policy.allowedBlocked, blockedItem.item);
225
+ const match = expectedWarning ?? allowedBlocked;
226
+ checks.push({
227
+ id: `execution-results.blocked.${blockedItem.artifact.fixture}.${blockedItem.item.seam}.${blockedItem.item.captureIndex}`,
228
+ action: match ? (options.strict ? "fail" : "warn") : "fail",
229
+ message: match
230
+ ? `${match.decision}: ${blockedItem.item.reason}`
231
+ : `unknown blocked synthetic probe: ${blockedItem.item.reason}`,
232
+ evidence: [
233
+ blockedItem.artifact.artifactPath,
234
+ blockedItem.item.seam,
235
+ blockedItem.item.reason,
236
+ match?.id ?? "unclassified",
237
+ ],
238
+ });
239
+ }
240
+ return checks;
241
+ }
242
+
243
+ function findPolicyMatch(rules, item) {
244
+ return rules.find((rule) => item.seam === rule.seam && item.reason?.includes(rule.reasonIncludes));
245
+ }
246
+
247
+ function failedExecutionEvidence(executionResults) {
248
+ return executionResults.artifacts.flatMap((artifact) =>
249
+ (artifact.failures ?? []).map((failure) => `${artifact.fixture}:${failure.seam}:${failure.error}`),
250
+ );
251
+ }
252
+
253
+ function actionRank(value) {
254
+ return { fail: 0, warn: 1, pass: 2 }[value] ?? 3;
255
+ }
256
+
257
+ function markdownTable(rows, headers) {
258
+ return renderPaddedMarkdownTable(rows, headers);
259
+ }
@@ -0,0 +1,223 @@
1
+ import { existsSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { renderPaddedMarkdownTable, writeJsonMarkdownArtifacts } from "./artifacts.js";
4
+ import { readOptionalJsonFile } from "./json-file.js";
5
+
6
+ export const defaultCiReportPaths = {
7
+ compatibility: "reports/plugin-inspector-report.json",
8
+ capture: "reports/plugin-inspector-capture.json",
9
+ synthetic: "reports/plugin-inspector-synthetic-probes.json",
10
+ coldImport: "reports/plugin-inspector-cold-import.json",
11
+ workspace: "reports/plugin-inspector-workspace-plan.json",
12
+ platform: "reports/plugin-inspector-platform-probes.json",
13
+ importLoop: "reports/plugin-inspector-import-loop-profile.json",
14
+ execution: "reports/plugin-inspector-execution-results.json",
15
+ runtimeProfile: "reports/plugin-inspector-runtime-profile.json",
16
+ refDiff: "reports/plugin-inspector-ref-diff.json",
17
+ profileDiff: "reports/plugin-inspector-profile-diff.json",
18
+ ciPolicy: "reports/plugin-inspector-ci-policy.json",
19
+ };
20
+
21
+ export async function buildCiSummary(options = {}) {
22
+ const reportPaths = options.reportPaths ?? defaultCiReportPaths;
23
+ const reports = options.reports ?? (await readCiReports(options.reportsDir ?? "reports", reportPaths));
24
+ const artifactBaseDir = options.artifactBaseDir ?? process.cwd();
25
+
26
+ return {
27
+ generatedAt: options.generatedAt ?? "deterministic",
28
+ title: options.title ?? "Plugin Inspector CI Summary",
29
+ mode: options.mode ?? "local",
30
+ openclawLabel: options.openclawLabel ?? "",
31
+ status: deriveCiStatus(reports),
32
+ summary: {
33
+ breakages: reports.compatibility?.summary?.breakageCount ?? 0,
34
+ warnings: reports.compatibility?.summary?.warningCount ?? 0,
35
+ suggestions: reports.compatibility?.summary?.suggestionCount ?? 0,
36
+ issues: reports.compatibility?.summary?.issueCount ?? 0,
37
+ p0Issues: reports.compatibility?.summary?.p0IssueCount ?? 0,
38
+ p1Issues: reports.compatibility?.summary?.p1IssueCount ?? 0,
39
+ liveIssues: reports.compatibility?.summary?.liveIssueCount ?? 0,
40
+ liveP0Issues: reports.compatibility?.summary?.liveP0IssueCount ?? 0,
41
+ compatGaps: reports.compatibility?.summary?.compatGapCount ?? 0,
42
+ deprecationWarnings: reports.compatibility?.summary?.deprecationWarningCount ?? 0,
43
+ inspectorGaps: reports.compatibility?.summary?.inspectorGapCount ?? 0,
44
+ upstreamIssues: reports.compatibility?.summary?.upstreamIssueCount ?? 0,
45
+ refDiffFailures: reports.refDiff?.summary?.hardRegressionCount ?? 0,
46
+ refDiffWarnings: reports.refDiff?.summary?.warningRegressionCount ?? 0,
47
+ policyFailures: reports.ciPolicy?.summary?.failCount ?? 0,
48
+ policyWarnings: reports.ciPolicy?.summary?.warnCount ?? 0,
49
+ profileFailures: reports.profileDiff?.summary?.failCount ?? 0,
50
+ profileWarnings: reports.profileDiff?.summary?.warnCount ?? 0,
51
+ executionPass: reports.execution?.summary?.passCount ?? 0,
52
+ executionFail: reports.execution?.summary?.failCount ?? 0,
53
+ executionBlocked: reports.execution?.summary?.blockedCount ?? 0,
54
+ platformWindowsRisks: reports.platform?.summary?.windowsRiskStepCount ?? 0,
55
+ platformContainerRisks: reports.platform?.summary?.containerRiskStepCount ?? 0,
56
+ loaderJitiCandidates: reports.platform?.summary?.jitiAlternativeCount ?? 0,
57
+ importLoopP50Ms: reports.importLoop?.summary?.p50WallMs ?? 0,
58
+ importLoopP95Ms: reports.importLoop?.summary?.p95WallMs ?? 0,
59
+ importLoopMaxRssMb: reports.importLoop?.summary?.maxPeakRssMb ?? 0,
60
+ importLoopMaxCpuMs: reports.importLoop?.summary?.maxCpuMsEstimate ?? 0,
61
+ },
62
+ topIssues: topIssues(reports.compatibility),
63
+ refRegressions: (reports.refDiff?.regressions ?? []).slice(0, 20),
64
+ policyFindings: (reports.ciPolicy?.checks ?? []).filter((check) => check.action !== "pass").slice(0, 20),
65
+ profileFindings: (reports.profileDiff?.checks ?? []).filter((check) => check.action !== "pass").slice(0, 20),
66
+ artifacts: Object.fromEntries(
67
+ Object.entries(reportPaths).map(([key, value]) => [key, existsSync(path.join(artifactBaseDir, value)) ? value : null]),
68
+ ),
69
+ };
70
+ }
71
+
72
+ export async function readCiReports(reportsDir, reportPaths = defaultCiReportPaths) {
73
+ const reports = {};
74
+ for (const [key, defaultPath] of Object.entries(reportPaths)) {
75
+ const reportPath = path.join(reportsDir, path.basename(defaultPath));
76
+ reports[key] = await readOptionalJsonFile(reportPath);
77
+ }
78
+ return reports;
79
+ }
80
+
81
+ export function deriveCiStatus(reports) {
82
+ if ((reports.compatibility?.summary?.breakageCount ?? 0) > 0) {
83
+ return "fail";
84
+ }
85
+ if ((reports.refDiff?.summary?.hardRegressionCount ?? 0) > 0) {
86
+ return "fail";
87
+ }
88
+ if ((reports.ciPolicy?.summary?.failCount ?? 0) > 0) {
89
+ return "fail";
90
+ }
91
+ if ((reports.profileDiff?.summary?.failCount ?? 0) > 0) {
92
+ return "fail";
93
+ }
94
+ if ((reports.execution?.summary?.failCount ?? 0) > 0) {
95
+ return "fail";
96
+ }
97
+ return "pass";
98
+ }
99
+
100
+ export async function writeCiSummary(summary, options = {}) {
101
+ const jsonPath = options.jsonPath ?? path.join(process.cwd(), "reports/plugin-inspector-ci-summary.json");
102
+ const markdownPath = options.markdownPath ?? path.join(process.cwd(), "reports/plugin-inspector-ci-summary.md");
103
+ return writeJsonMarkdownArtifacts({
104
+ jsonPath,
105
+ markdownPath,
106
+ json: summary,
107
+ markdown: renderCiSummaryMarkdown(summary),
108
+ check: options.check,
109
+ });
110
+ }
111
+
112
+ export function renderCiSummaryMarkdown(summary) {
113
+ return [
114
+ `# ${summary.title ?? "Plugin Inspector CI Summary"}`,
115
+ "",
116
+ `Generated: ${summary.generatedAt}`,
117
+ `Mode: ${summary.mode}`,
118
+ `OpenClaw: ${summary.openclawLabel || "-"}`,
119
+ `Status: ${summary.status.toUpperCase()}`,
120
+ "",
121
+ "## Counts",
122
+ "",
123
+ markdownTable(
124
+ [
125
+ ["Breakages", summary.summary.breakages],
126
+ ["Warnings", summary.summary.warnings],
127
+ ["Suggestions", summary.summary.suggestions],
128
+ ["Issues", summary.summary.issues],
129
+ ["P0 issues", summary.summary.p0Issues],
130
+ ["P1 issues", summary.summary.p1Issues],
131
+ ["Live issues", summary.summary.liveIssues],
132
+ ["Live P0 issues", summary.summary.liveP0Issues],
133
+ ["Compat gaps", summary.summary.compatGaps],
134
+ ["Deprecation warnings", summary.summary.deprecationWarnings],
135
+ ["Inspector gaps", summary.summary.inspectorGaps],
136
+ ["Upstream metadata", summary.summary.upstreamIssues],
137
+ ["Ref diff failures", summary.summary.refDiffFailures],
138
+ ["Ref diff warnings", summary.summary.refDiffWarnings],
139
+ ["Policy failures", summary.summary.policyFailures],
140
+ ["Policy warnings", summary.summary.policyWarnings],
141
+ ["Profile failures", summary.summary.profileFailures],
142
+ ["Profile warnings", summary.summary.profileWarnings],
143
+ ["Execution pass", summary.summary.executionPass],
144
+ ["Execution fail", summary.summary.executionFail],
145
+ ["Execution blocked", summary.summary.executionBlocked],
146
+ ["Windows portability risks", summary.summary.platformWindowsRisks],
147
+ ["Container portability risks", summary.summary.platformContainerRisks],
148
+ ["Jiti loader candidates", summary.summary.loaderJitiCandidates],
149
+ [
150
+ "Import loop",
151
+ `p50 ${summary.summary.importLoopP50Ms} ms / p95 ${summary.summary.importLoopP95Ms} ms / max RSS ${summary.summary.importLoopMaxRssMb} MB / CPU ${summary.summary.importLoopMaxCpuMs} ms`,
152
+ ],
153
+ ],
154
+ ["Metric", "Value"],
155
+ ),
156
+ "",
157
+ "## Top Issues",
158
+ "",
159
+ markdownTable(
160
+ summary.topIssues.map((issue) => [issue.severity, issue.issueClass ?? "-", issue.fixture, issue.code, issue.decision, issue.title]),
161
+ ["Severity", "Class", "Fixture", "Code", "Decision", "Title"],
162
+ ),
163
+ "",
164
+ "## Ref Regressions",
165
+ "",
166
+ markdownTable(
167
+ summary.refRegressions.map((regression) => [
168
+ regression.action,
169
+ regression.severity,
170
+ regression.dimension,
171
+ regression.code,
172
+ regression.message,
173
+ ]),
174
+ ["Action", "Severity", "Surface", "Code", "Message"],
175
+ ),
176
+ "",
177
+ "## Policy Findings",
178
+ "",
179
+ markdownTable(
180
+ summary.policyFindings.map((finding) => [finding.action, finding.id, finding.message, finding.evidence.join(", ")]),
181
+ ["Action", "ID", "Message", "Evidence"],
182
+ ),
183
+ "",
184
+ "## Profile Findings",
185
+ "",
186
+ markdownTable(
187
+ summary.profileFindings.map((finding) => [
188
+ finding.action,
189
+ finding.id,
190
+ finding.metric,
191
+ finding.baseline ?? "-",
192
+ finding.current ?? "-",
193
+ finding.message,
194
+ ]),
195
+ ["Action", "ID", "Metric", "Baseline", "Current", "Message"],
196
+ ),
197
+ "",
198
+ "## Artifacts",
199
+ "",
200
+ markdownTable(
201
+ Object.entries(summary.artifacts).map(([key, value]) => [key, value ?? "-"]),
202
+ ["Artifact", "Path"],
203
+ ),
204
+ ].join("\n");
205
+ }
206
+
207
+ function topIssues(report) {
208
+ return (report?.issues ?? [])
209
+ .filter((issue) => ["P0", "P1"].includes(issue.severity))
210
+ .slice(0, 20)
211
+ .map((issue) => ({
212
+ severity: issue.severity,
213
+ issueClass: issue.issueClass,
214
+ fixture: issue.fixture,
215
+ code: issue.code,
216
+ title: issue.title,
217
+ decision: issue.decision,
218
+ }));
219
+ }
220
+
221
+ function markdownTable(rows, headers) {
222
+ return renderPaddedMarkdownTable(rows, headers, { nullValue: "-" });
223
+ }