@vertaaux/cli 0.2.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.
Files changed (198) hide show
  1. package/README.md +345 -0
  2. package/dist/auth/ci-token.d.ts +49 -0
  3. package/dist/auth/ci-token.d.ts.map +1 -0
  4. package/dist/auth/ci-token.js +83 -0
  5. package/dist/auth/device-flow.d.ts +66 -0
  6. package/dist/auth/device-flow.d.ts.map +1 -0
  7. package/dist/auth/device-flow.js +156 -0
  8. package/dist/auth/token-store.d.ts +53 -0
  9. package/dist/auth/token-store.d.ts.map +1 -0
  10. package/dist/auth/token-store.js +78 -0
  11. package/dist/baseline/diff.d.ts +57 -0
  12. package/dist/baseline/diff.d.ts.map +1 -0
  13. package/dist/baseline/diff.js +152 -0
  14. package/dist/baseline/hash.d.ts +54 -0
  15. package/dist/baseline/hash.d.ts.map +1 -0
  16. package/dist/baseline/hash.js +66 -0
  17. package/dist/baseline/manager.d.ts +89 -0
  18. package/dist/baseline/manager.d.ts.map +1 -0
  19. package/dist/baseline/manager.js +157 -0
  20. package/dist/cache/index.d.ts +8 -0
  21. package/dist/cache/index.d.ts.map +1 -0
  22. package/dist/cache/index.js +7 -0
  23. package/dist/cache/route-cache.d.ts +119 -0
  24. package/dist/cache/route-cache.d.ts.map +1 -0
  25. package/dist/cache/route-cache.js +213 -0
  26. package/dist/ci/changed-routes.d.ts +95 -0
  27. package/dist/ci/changed-routes.d.ts.map +1 -0
  28. package/dist/ci/changed-routes.js +304 -0
  29. package/dist/ci/github-api.d.ts +68 -0
  30. package/dist/ci/github-api.d.ts.map +1 -0
  31. package/dist/ci/github-api.js +138 -0
  32. package/dist/ci/gitlab-api.d.ts +75 -0
  33. package/dist/ci/gitlab-api.d.ts.map +1 -0
  34. package/dist/ci/gitlab-api.js +180 -0
  35. package/dist/ci/index.d.ts +6 -0
  36. package/dist/ci/index.d.ts.map +1 -0
  37. package/dist/ci/index.js +4 -0
  38. package/dist/commands/audit.d.ts +58 -0
  39. package/dist/commands/audit.d.ts.map +1 -0
  40. package/dist/commands/audit.js +862 -0
  41. package/dist/commands/baseline.d.ts +22 -0
  42. package/dist/commands/baseline.d.ts.map +1 -0
  43. package/dist/commands/baseline.js +210 -0
  44. package/dist/commands/comment.d.ts +14 -0
  45. package/dist/commands/comment.d.ts.map +1 -0
  46. package/dist/commands/comment.js +363 -0
  47. package/dist/commands/diff.d.ts +24 -0
  48. package/dist/commands/diff.d.ts.map +1 -0
  49. package/dist/commands/diff.js +196 -0
  50. package/dist/commands/doctor.d.ts +58 -0
  51. package/dist/commands/doctor.d.ts.map +1 -0
  52. package/dist/commands/doctor.js +338 -0
  53. package/dist/commands/download.d.ts +12 -0
  54. package/dist/commands/download.d.ts.map +1 -0
  55. package/dist/commands/download.js +183 -0
  56. package/dist/commands/explain.d.ts +62 -0
  57. package/dist/commands/explain.d.ts.map +1 -0
  58. package/dist/commands/explain.js +302 -0
  59. package/dist/commands/init.d.ts +12 -0
  60. package/dist/commands/init.d.ts.map +1 -0
  61. package/dist/commands/init.js +212 -0
  62. package/dist/commands/login.d.ts +14 -0
  63. package/dist/commands/login.d.ts.map +1 -0
  64. package/dist/commands/login.js +222 -0
  65. package/dist/commands/policy.d.ts +13 -0
  66. package/dist/commands/policy.d.ts.map +1 -0
  67. package/dist/commands/policy.js +347 -0
  68. package/dist/commands/upload.d.ts +12 -0
  69. package/dist/commands/upload.d.ts.map +1 -0
  70. package/dist/commands/upload.js +158 -0
  71. package/dist/config/defaults.d.ts +21 -0
  72. package/dist/config/defaults.d.ts.map +1 -0
  73. package/dist/config/defaults.js +49 -0
  74. package/dist/config/loader.d.ts +66 -0
  75. package/dist/config/loader.d.ts.map +1 -0
  76. package/dist/config/loader.js +167 -0
  77. package/dist/config/schema.d.ts +55 -0
  78. package/dist/config/schema.d.ts.map +1 -0
  79. package/dist/config/schema.js +6 -0
  80. package/dist/index.d.ts +9 -0
  81. package/dist/index.d.ts.map +1 -0
  82. package/dist/index.js +1090 -0
  83. package/dist/interactive/fix-wizard.d.ts +44 -0
  84. package/dist/interactive/fix-wizard.d.ts.map +1 -0
  85. package/dist/interactive/fix-wizard.js +286 -0
  86. package/dist/interactive/init-wizard.d.ts +32 -0
  87. package/dist/interactive/init-wizard.d.ts.map +1 -0
  88. package/dist/interactive/init-wizard.js +193 -0
  89. package/dist/interactive/prompts.d.ts +62 -0
  90. package/dist/interactive/prompts.d.ts.map +1 -0
  91. package/dist/interactive/prompts.js +78 -0
  92. package/dist/monorepo/detector.d.ts +70 -0
  93. package/dist/monorepo/detector.d.ts.map +1 -0
  94. package/dist/monorepo/detector.js +278 -0
  95. package/dist/monorepo/index.d.ts +9 -0
  96. package/dist/monorepo/index.d.ts.map +1 -0
  97. package/dist/monorepo/index.js +8 -0
  98. package/dist/monorepo/workspace.d.ts +142 -0
  99. package/dist/monorepo/workspace.d.ts.map +1 -0
  100. package/dist/monorepo/workspace.js +171 -0
  101. package/dist/output/envelope.d.ts +21 -0
  102. package/dist/output/envelope.d.ts.map +1 -0
  103. package/dist/output/envelope.js +27 -0
  104. package/dist/output/factory.d.ts +73 -0
  105. package/dist/output/factory.d.ts.map +1 -0
  106. package/dist/output/factory.js +60 -0
  107. package/dist/output/formats.d.ts +11 -0
  108. package/dist/output/formats.d.ts.map +1 -0
  109. package/dist/output/formats.js +41 -0
  110. package/dist/output/html.d.ts +45 -0
  111. package/dist/output/html.d.ts.map +1 -0
  112. package/dist/output/html.js +607 -0
  113. package/dist/output/human.d.ts +41 -0
  114. package/dist/output/human.d.ts.map +1 -0
  115. package/dist/output/human.js +274 -0
  116. package/dist/output/json.d.ts +42 -0
  117. package/dist/output/json.d.ts.map +1 -0
  118. package/dist/output/json.js +37 -0
  119. package/dist/output/junit.d.ts +56 -0
  120. package/dist/output/junit.d.ts.map +1 -0
  121. package/dist/output/junit.js +135 -0
  122. package/dist/output/markdown.d.ts +77 -0
  123. package/dist/output/markdown.d.ts.map +1 -0
  124. package/dist/output/markdown.js +411 -0
  125. package/dist/output/sarif.d.ts +160 -0
  126. package/dist/output/sarif.d.ts.map +1 -0
  127. package/dist/output/sarif.js +207 -0
  128. package/dist/policy/evaluator.d.ts +111 -0
  129. package/dist/policy/evaluator.d.ts.map +1 -0
  130. package/dist/policy/evaluator.js +362 -0
  131. package/dist/policy/index.d.ts +15 -0
  132. package/dist/policy/index.d.ts.map +1 -0
  133. package/dist/policy/index.js +11 -0
  134. package/dist/policy/loader.d.ts +97 -0
  135. package/dist/policy/loader.d.ts.map +1 -0
  136. package/dist/policy/loader.js +281 -0
  137. package/dist/policy/schema.d.ts +297 -0
  138. package/dist/policy/schema.d.ts.map +1 -0
  139. package/dist/policy/schema.js +230 -0
  140. package/dist/quality-gate/evaluator.d.ts +58 -0
  141. package/dist/quality-gate/evaluator.d.ts.map +1 -0
  142. package/dist/quality-gate/evaluator.js +274 -0
  143. package/dist/quality-gate/index.d.ts +10 -0
  144. package/dist/quality-gate/index.d.ts.map +1 -0
  145. package/dist/quality-gate/index.js +7 -0
  146. package/dist/quality-gate/types.d.ts +103 -0
  147. package/dist/quality-gate/types.d.ts.map +1 -0
  148. package/dist/quality-gate/types.js +23 -0
  149. package/dist/templates/azure-devops.d.ts +25 -0
  150. package/dist/templates/azure-devops.d.ts.map +1 -0
  151. package/dist/templates/azure-devops.js +109 -0
  152. package/dist/templates/circleci.d.ts +28 -0
  153. package/dist/templates/circleci.d.ts.map +1 -0
  154. package/dist/templates/circleci.js +86 -0
  155. package/dist/templates/github-actions.d.ts +81 -0
  156. package/dist/templates/github-actions.d.ts.map +1 -0
  157. package/dist/templates/github-actions.js +393 -0
  158. package/dist/templates/gitlab-ci.d.ts +26 -0
  159. package/dist/templates/gitlab-ci.d.ts.map +1 -0
  160. package/dist/templates/gitlab-ci.js +70 -0
  161. package/dist/templates/index.d.ts +72 -0
  162. package/dist/templates/index.d.ts.map +1 -0
  163. package/dist/templates/index.js +112 -0
  164. package/dist/templates/jenkins.d.ts +26 -0
  165. package/dist/templates/jenkins.d.ts.map +1 -0
  166. package/dist/templates/jenkins.js +110 -0
  167. package/dist/ui/banner.d.ts +31 -0
  168. package/dist/ui/banner.d.ts.map +1 -0
  169. package/dist/ui/banner.js +84 -0
  170. package/dist/ui/diagnostics.d.ts +39 -0
  171. package/dist/ui/diagnostics.d.ts.map +1 -0
  172. package/dist/ui/diagnostics.js +153 -0
  173. package/dist/ui/spinner.d.ts +61 -0
  174. package/dist/ui/spinner.d.ts.map +1 -0
  175. package/dist/ui/spinner.js +101 -0
  176. package/dist/ui/table.d.ts +63 -0
  177. package/dist/ui/table.d.ts.map +1 -0
  178. package/dist/ui/table.js +236 -0
  179. package/dist/utils/client.d.ts +82 -0
  180. package/dist/utils/client.d.ts.map +1 -0
  181. package/dist/utils/client.js +128 -0
  182. package/dist/utils/detect-env.d.ts +59 -0
  183. package/dist/utils/detect-env.d.ts.map +1 -0
  184. package/dist/utils/detect-env.js +115 -0
  185. package/dist/utils/exit-codes.d.ts +47 -0
  186. package/dist/utils/exit-codes.d.ts.map +1 -0
  187. package/dist/utils/exit-codes.js +61 -0
  188. package/dist/utils/logger.d.ts +87 -0
  189. package/dist/utils/logger.d.ts.map +1 -0
  190. package/dist/utils/logger.js +185 -0
  191. package/dist/utils/sanitize.d.ts +36 -0
  192. package/dist/utils/sanitize.d.ts.map +1 -0
  193. package/dist/utils/sanitize.js +64 -0
  194. package/dist/utils/validators.d.ts +41 -0
  195. package/dist/utils/validators.d.ts.map +1 -0
  196. package/dist/utils/validators.js +123 -0
  197. package/package.json +63 -0
  198. package/schemas/vertaaux.config.schema.json +103 -0
@@ -0,0 +1,411 @@
1
+ /**
2
+ * Markdown formatter for PR comments.
3
+ *
4
+ * Generates GitHub/GitLab flavored markdown with:
5
+ * - Collapsible sections for grouping issues
6
+ * - Evidence links for each issue
7
+ * - New/fixed issue labeling from baseline comparison
8
+ * - Hidden identifier for sticky comment matching
9
+ */
10
+ import { generateFingerprint } from "../baseline/hash.js";
11
+ /**
12
+ * Default markdown options.
13
+ */
14
+ export const DEFAULT_MARKDOWN_OPTIONS = {
15
+ groupBy: "severity",
16
+ collapse: true,
17
+ collapseThreshold: 3,
18
+ includeEvidence: true,
19
+ includeFixes: true,
20
+ baseUrl: "https://vertaaux.ai",
21
+ };
22
+ /**
23
+ * Emoji mappings for severities.
24
+ */
25
+ const SEVERITY_EMOJI = {
26
+ error: ":red_circle:",
27
+ critical: ":red_circle:",
28
+ warning: ":yellow_circle:",
29
+ serious: ":yellow_circle:",
30
+ info: ":blue_circle:",
31
+ minor: ":blue_circle:",
32
+ moderate: ":large_orange_circle:",
33
+ };
34
+ /**
35
+ * Status emoji mappings.
36
+ */
37
+ const STATUS_EMOJI = {
38
+ success: ":white_check_mark:",
39
+ warning: ":warning:",
40
+ error: ":x:",
41
+ new: ":new:",
42
+ fixed: ":white_check_mark:",
43
+ };
44
+ /**
45
+ * Normalize severity to standard values.
46
+ */
47
+ function normalizeSeverity(severity) {
48
+ const sev = (severity || "info").toLowerCase();
49
+ if (sev === "critical")
50
+ return "error";
51
+ if (sev === "serious")
52
+ return "warning";
53
+ if (sev === "minor" || sev === "moderate")
54
+ return "info";
55
+ return sev;
56
+ }
57
+ /**
58
+ * Get rule ID from issue.
59
+ */
60
+ function getRuleId(issue) {
61
+ if ("ruleId" in issue && issue.ruleId)
62
+ return issue.ruleId;
63
+ if ("rule_id" in issue && issue.rule_id)
64
+ return issue.rule_id;
65
+ if ("id" in issue && issue.id)
66
+ return issue.id;
67
+ return "unknown";
68
+ }
69
+ /**
70
+ * Count issues by severity.
71
+ */
72
+ function countIssues(issues) {
73
+ const counts = { error: 0, warning: 0, info: 0, total: 0 };
74
+ for (const issue of issues) {
75
+ const sev = normalizeSeverity(issue.severity);
76
+ if (sev === "error")
77
+ counts.error++;
78
+ else if (sev === "warning")
79
+ counts.warning++;
80
+ else
81
+ counts.info++;
82
+ counts.total++;
83
+ }
84
+ return counts;
85
+ }
86
+ /**
87
+ * Extract grouping key from issue.
88
+ */
89
+ function getGroupKey(issue, groupBy) {
90
+ switch (groupBy) {
91
+ case "severity":
92
+ return normalizeSeverity(issue.severity);
93
+ case "category":
94
+ return issue.category || "General";
95
+ case "route": {
96
+ // Extract route from selector or URL
97
+ const routeMatch = issue.selector?.match(/\[data-route="([^"]+)"\]/);
98
+ if (routeMatch)
99
+ return routeMatch[1];
100
+ // Fall back to extracting path from description if it contains URL-like paths
101
+ const pathMatch = issue.description?.match(/\/[a-z0-9-/]+/i);
102
+ return pathMatch ? pathMatch[0] : "/";
103
+ }
104
+ case "file": {
105
+ // Try to extract file path from issue
106
+ const fileIssue = issue;
107
+ return fileIssue.file || fileIssue.filePath || "Unknown File";
108
+ }
109
+ case "component": {
110
+ // Extract component name from selector or class
111
+ const classMatch = issue.selector?.match(/\.([A-Z][a-zA-Z0-9]+)/);
112
+ if (classMatch)
113
+ return classMatch[1];
114
+ // Try data-component attribute
115
+ const compMatch = issue.selector?.match(/\[data-component="([^"]+)"\]/);
116
+ if (compMatch)
117
+ return compMatch[1];
118
+ // Try extracting from tag name patterns like Button, Card, etc
119
+ const tagMatch = issue.selector?.match(/(?:^|\s)([A-Z][a-zA-Z0-9]+)(?:\[|\.|\s|$)/);
120
+ return tagMatch ? tagMatch[1] : "General";
121
+ }
122
+ default:
123
+ return "General";
124
+ }
125
+ }
126
+ /**
127
+ * Group issues by the specified field.
128
+ */
129
+ function groupIssues(issues, groupBy) {
130
+ const groups = new Map();
131
+ for (const issue of issues) {
132
+ const key = getGroupKey(issue, groupBy);
133
+ if (!groups.has(key)) {
134
+ groups.set(key, []);
135
+ }
136
+ groups.get(key).push(issue);
137
+ }
138
+ return groups;
139
+ }
140
+ /**
141
+ * Sort group keys for consistent ordering.
142
+ */
143
+ function sortGroupKeys(keys, groupBy) {
144
+ if (groupBy === "severity") {
145
+ const priority = { error: 0, warning: 1, info: 2 };
146
+ return keys.sort((a, b) => (priority[a] ?? 3) - (priority[b] ?? 3));
147
+ }
148
+ return keys.sort((a, b) => a.localeCompare(b));
149
+ }
150
+ /**
151
+ * Format severity label for display.
152
+ */
153
+ function formatSeverityLabel(severity) {
154
+ const normalized = normalizeSeverity(severity);
155
+ return normalized.charAt(0).toUpperCase() + normalized.slice(1);
156
+ }
157
+ /**
158
+ * Format a single issue as markdown.
159
+ */
160
+ function formatIssue(issue, options) {
161
+ const emoji = SEVERITY_EMOJI[normalizeSeverity(issue.severity)] || ":grey_question:";
162
+ const severity = formatSeverityLabel(issue.severity || "info");
163
+ const ruleId = getRuleId(issue);
164
+ const description = issue.description || issue.title || ruleId;
165
+ const fingerprint = generateFingerprint(issue);
166
+ const lines = [];
167
+ // Header with severity and rule
168
+ lines.push(`#### ${emoji} ${severity}: ${ruleId}`);
169
+ lines.push(`**Description:** ${description}`);
170
+ if (issue.selector) {
171
+ lines.push(`**Selector:** \`${issue.selector}\``);
172
+ }
173
+ // Fix suggestion if available and enabled
174
+ if (options.includeFixes) {
175
+ const fix = issue.recommendation || issue.recommended_fix;
176
+ if (fix) {
177
+ lines.push(`**Fix:** ${fix}`);
178
+ }
179
+ }
180
+ // WCAG reference if available
181
+ if (issue.wcag_reference) {
182
+ lines.push(`**WCAG:** ${issue.wcag_reference}`);
183
+ }
184
+ // Evidence link if enabled
185
+ if (options.includeEvidence) {
186
+ lines.push(`[View evidence](${options.baseUrl}/evidence/${fingerprint})`);
187
+ }
188
+ return lines.join("\n");
189
+ }
190
+ /**
191
+ * Format fixed issue summary.
192
+ */
193
+ function formatFixedIssue(issue) {
194
+ const ruleId = issue.ruleId || "unknown";
195
+ const description = issue.description || ruleId;
196
+ return `${STATUS_EMOJI.fixed} ~~${ruleId}~~: ${description}`;
197
+ }
198
+ /**
199
+ * Generate status line based on issue counts.
200
+ */
201
+ function generateStatusLine(newCount, fixedCount, existingCount) {
202
+ if (newCount === 0 && fixedCount === 0 && existingCount === 0) {
203
+ return `**Status:** :green_circle: All clear! No issues found.`;
204
+ }
205
+ const parts = [];
206
+ if (newCount > 0) {
207
+ parts.push(`${STATUS_EMOJI.warning} ${newCount} new issue${newCount === 1 ? "" : "s"} found`);
208
+ }
209
+ else {
210
+ parts.push(`:green_circle: No new issues`);
211
+ }
212
+ if (fixedCount > 0) {
213
+ parts.push(`${STATUS_EMOJI.fixed} ${fixedCount} fixed`);
214
+ }
215
+ return `**Status:** ${parts.join(" | ")}`;
216
+ }
217
+ /**
218
+ * Generate summary table.
219
+ */
220
+ function generateSummaryTable(newIssues, existingIssues, groupBy) {
221
+ const lines = [];
222
+ if (groupBy === "category") {
223
+ // Category-based summary
224
+ const categories = new Set();
225
+ [...newIssues, ...existingIssues].forEach((i) => {
226
+ categories.add(i.category || "General");
227
+ });
228
+ const newByCategory = new Map();
229
+ const existingByCategory = new Map();
230
+ newIssues.forEach((i) => {
231
+ const cat = i.category || "General";
232
+ newByCategory.set(cat, (newByCategory.get(cat) || 0) + 1);
233
+ });
234
+ existingIssues.forEach((i) => {
235
+ const cat = i.category || "General";
236
+ existingByCategory.set(cat, (existingByCategory.get(cat) || 0) + 1);
237
+ });
238
+ lines.push("| Category | New | Existing | Total |");
239
+ lines.push("|----------|-----|----------|-------|");
240
+ for (const cat of Array.from(categories).sort()) {
241
+ const newCount = newByCategory.get(cat) || 0;
242
+ const existingCount = existingByCategory.get(cat) || 0;
243
+ lines.push(`| ${cat} | ${newCount} | ${existingCount} | ${newCount + existingCount} |`);
244
+ }
245
+ }
246
+ else {
247
+ // Default: severity-based summary
248
+ const newCounts = countIssues(newIssues);
249
+ const existingCounts = countIssues(existingIssues);
250
+ lines.push("| Severity | New | Existing | Total |");
251
+ lines.push("|----------|-----|----------|-------|");
252
+ lines.push(`| :red_circle: Error | ${newCounts.error} | ${existingCounts.error} | ${newCounts.error + existingCounts.error} |`);
253
+ lines.push(`| :yellow_circle: Warning | ${newCounts.warning} | ${existingCounts.warning} | ${newCounts.warning + existingCounts.warning} |`);
254
+ lines.push(`| :blue_circle: Info | ${newCounts.info} | ${existingCounts.info} | ${newCounts.info + existingCounts.info} |`);
255
+ }
256
+ return lines;
257
+ }
258
+ /**
259
+ * Generate scores section.
260
+ */
261
+ function generateScoresSection(scores) {
262
+ if (!scores)
263
+ return [];
264
+ const parts = [];
265
+ if (scores.overall !== undefined) {
266
+ parts.push(`**Overall:** ${scores.overall}/100`);
267
+ }
268
+ if (scores.accessibility !== undefined) {
269
+ parts.push(`**Accessibility:** ${scores.accessibility}/100`);
270
+ }
271
+ if (parts.length === 0)
272
+ return [];
273
+ return ["### Scores", "", parts.join(" | "), ""];
274
+ }
275
+ /**
276
+ * Format markdown comment from audit data.
277
+ *
278
+ * Produces GitHub-flavored markdown with:
279
+ * - Hidden identifier for sticky comment matching
280
+ * - Status summary line
281
+ * - Issue counts table
282
+ * - Grouped and optionally collapsible issue sections
283
+ * - Evidence links for each issue
284
+ * - Footer with audit ID links
285
+ *
286
+ * @param data - Comment data with categorized issues
287
+ * @param options - Formatting options
288
+ * @returns Formatted markdown string
289
+ */
290
+ export function formatMarkdownComment(data, options = {}) {
291
+ const opts = { ...DEFAULT_MARKDOWN_OPTIONS, ...options };
292
+ const { newIssues, fixedIssues, existingIssues, auditId, scores } = data;
293
+ // Determine if we should collapse based on threshold
294
+ const shouldCollapse = opts.collapse && newIssues.length > opts.collapseThreshold;
295
+ const lines = [];
296
+ // Hidden identifier for sticky comment matching (CRITICAL)
297
+ lines.push("<!-- vertaaux-audit -->");
298
+ lines.push("## VertaaUX Audit Results");
299
+ lines.push("");
300
+ // Status line
301
+ lines.push(generateStatusLine(newIssues.length, fixedIssues.length, existingIssues.length));
302
+ lines.push("");
303
+ // Summary table
304
+ if (newIssues.length > 0 || existingIssues.length > 0) {
305
+ lines.push("### Summary");
306
+ lines.push("");
307
+ lines.push(...generateSummaryTable(newIssues, existingIssues, opts.groupBy));
308
+ lines.push("");
309
+ }
310
+ // Scores section
311
+ lines.push(...generateScoresSection(scores));
312
+ // New issues section
313
+ if (newIssues.length > 0) {
314
+ const grouped = groupIssues(newIssues, opts.groupBy);
315
+ const sortedKeys = sortGroupKeys(Array.from(grouped.keys()), opts.groupBy);
316
+ if (shouldCollapse) {
317
+ lines.push("<details>");
318
+ lines.push(`<summary><strong>${STATUS_EMOJI.new} New Issues (${newIssues.length})</strong></summary>`);
319
+ lines.push("");
320
+ }
321
+ else {
322
+ lines.push(`### ${STATUS_EMOJI.new} New Issues (${newIssues.length})`);
323
+ lines.push("");
324
+ }
325
+ for (const groupKey of sortedKeys) {
326
+ const groupedIssues = grouped.get(groupKey);
327
+ // Add group header if grouping by something other than severity with single group
328
+ if (sortedKeys.length > 1 || opts.groupBy !== "severity") {
329
+ const groupLabel = opts.groupBy === "severity"
330
+ ? formatSeverityLabel(groupKey)
331
+ : groupKey;
332
+ lines.push(`**${groupLabel}** (${groupedIssues.length})`);
333
+ lines.push("");
334
+ }
335
+ for (const issue of groupedIssues) {
336
+ lines.push(formatIssue(issue, opts));
337
+ lines.push("");
338
+ }
339
+ }
340
+ if (shouldCollapse) {
341
+ lines.push("</details>");
342
+ lines.push("");
343
+ }
344
+ }
345
+ // Fixed issues section (always collapsible)
346
+ if (fixedIssues.length > 0) {
347
+ lines.push("<details>");
348
+ lines.push(`<summary><strong>${STATUS_EMOJI.fixed} Fixed Issues (${fixedIssues.length})</strong></summary>`);
349
+ lines.push("");
350
+ for (const issue of fixedIssues) {
351
+ lines.push(formatFixedIssue(issue));
352
+ }
353
+ lines.push("");
354
+ lines.push("</details>");
355
+ lines.push("");
356
+ }
357
+ // Existing issues count (collapsed, just a note)
358
+ if (existingIssues.length > 0) {
359
+ lines.push("<details>");
360
+ lines.push(`<summary><em>Existing Issues (${existingIssues.length} baselined)</em></summary>`);
361
+ lines.push("");
362
+ lines.push("These issues are tracked in baseline and will not fail the build.");
363
+ lines.push("");
364
+ lines.push("</details>");
365
+ lines.push("");
366
+ }
367
+ // Footer
368
+ lines.push("---");
369
+ if (auditId) {
370
+ lines.push(`*Audit ID: [${auditId}](${opts.baseUrl}/audit/${auditId}) | [Full Report](${opts.baseUrl}/report/${auditId})*`);
371
+ }
372
+ else {
373
+ const timestamp = new Date().toISOString().split("T")[0];
374
+ lines.push(`*Generated by [VertaaUX](${opts.baseUrl}) on ${timestamp}*`);
375
+ }
376
+ return lines.join("\n");
377
+ }
378
+ /**
379
+ * Categorize issues into new/existing/fixed based on baseline.
380
+ *
381
+ * @param currentIssues - Issues from current audit
382
+ * @param baseline - Baseline for comparison (null = all issues are new)
383
+ * @returns Categorized issues for comment data
384
+ */
385
+ export function categorizeIssuesForComment(currentIssues, baseline) {
386
+ if (!baseline) {
387
+ return {
388
+ newIssues: currentIssues,
389
+ fixedIssues: [],
390
+ existingIssues: [],
391
+ };
392
+ }
393
+ const baselineFingerprints = new Set(baseline.issues.map((i) => i.fingerprint));
394
+ const currentFingerprints = new Map();
395
+ for (const issue of currentIssues) {
396
+ currentFingerprints.set(generateFingerprint(issue), issue);
397
+ }
398
+ const newIssues = [];
399
+ const existingIssues = [];
400
+ for (const [fingerprint, issue] of currentFingerprints) {
401
+ if (baselineFingerprints.has(fingerprint)) {
402
+ existingIssues.push(issue);
403
+ }
404
+ else {
405
+ newIssues.push(issue);
406
+ }
407
+ }
408
+ // Fixed = in baseline but not in current
409
+ const fixedIssues = baseline.issues.filter((bi) => !currentFingerprints.has(bi.fingerprint));
410
+ return { newIssues, fixedIssues, existingIssues };
411
+ }
@@ -0,0 +1,160 @@
1
+ /**
2
+ * SARIF 2.1.0 output formatter for CLI.
3
+ *
4
+ * Generates SARIF (Static Analysis Results Interchange Format) output
5
+ * for integration with GitHub Code Scanning and other SARIF consumers.
6
+ *
7
+ * @see https://docs.oasis-open.org/sarif/sarif/v2.1.0/sarif-v2.1.0.html
8
+ * @see https://docs.github.com/en/code-security/code-scanning/integrating-with-code-scanning/sarif-support-for-code-scanning
9
+ */
10
+ /**
11
+ * SARIF log root structure.
12
+ */
13
+ export interface SarifLog {
14
+ $schema: string;
15
+ version: "2.1.0";
16
+ runs: SarifRun[];
17
+ }
18
+ /**
19
+ * SARIF run containing tool info and results.
20
+ */
21
+ export interface SarifRun {
22
+ tool: {
23
+ driver: SarifDriver;
24
+ };
25
+ invocation?: SarifInvocation;
26
+ results: SarifResult[];
27
+ }
28
+ /**
29
+ * SARIF tool driver information.
30
+ */
31
+ export interface SarifDriver {
32
+ name: string;
33
+ version: string;
34
+ informationUri: string;
35
+ rules: SarifRule[];
36
+ }
37
+ /**
38
+ * SARIF rule definition.
39
+ */
40
+ export interface SarifRule {
41
+ id: string;
42
+ name?: string;
43
+ shortDescription: {
44
+ text: string;
45
+ };
46
+ fullDescription?: {
47
+ text: string;
48
+ };
49
+ helpUri?: string;
50
+ defaultConfiguration?: {
51
+ level: SarifLevel;
52
+ };
53
+ properties?: {
54
+ precision?: "very-high" | "high" | "medium" | "low";
55
+ "security-severity"?: string;
56
+ category?: string;
57
+ };
58
+ }
59
+ /**
60
+ * SARIF result (finding/issue).
61
+ */
62
+ export interface SarifResult {
63
+ ruleId: string;
64
+ message: {
65
+ text: string;
66
+ };
67
+ level: SarifLevel;
68
+ locations: SarifLocation[];
69
+ partialFingerprints?: {
70
+ primaryLocationLineHash: string;
71
+ };
72
+ suppressions?: SarifSuppression[];
73
+ }
74
+ /**
75
+ * SARIF location for a result.
76
+ */
77
+ export interface SarifLocation {
78
+ physicalLocation: {
79
+ artifactLocation: {
80
+ uri: string;
81
+ };
82
+ region?: {
83
+ startLine: number;
84
+ startColumn?: number;
85
+ endLine?: number;
86
+ endColumn?: number;
87
+ };
88
+ };
89
+ }
90
+ /**
91
+ * SARIF suppression (for baselined issues).
92
+ */
93
+ export interface SarifSuppression {
94
+ kind: "inSource" | "external";
95
+ justification?: string;
96
+ }
97
+ /**
98
+ * SARIF invocation metadata.
99
+ */
100
+ export interface SarifInvocation {
101
+ workingDirectory?: {
102
+ uri: string;
103
+ };
104
+ executionSuccessful: boolean;
105
+ }
106
+ /**
107
+ * SARIF severity levels.
108
+ */
109
+ export type SarifLevel = "none" | "note" | "warning" | "error";
110
+ /**
111
+ * Options for SARIF formatting.
112
+ */
113
+ export interface SarifOptions {
114
+ /** Mark baselined issues as suppressed */
115
+ includeBaseline?: boolean;
116
+ /** Set of baselined fingerprints */
117
+ baselineFingerprints?: Set<string>;
118
+ /** Working directory for invocation */
119
+ workingDirectory?: string;
120
+ }
121
+ /**
122
+ * Audit result structure (matches factory.ts).
123
+ */
124
+ export interface AuditResult {
125
+ job_id?: string;
126
+ status?: string;
127
+ url?: string;
128
+ mode?: string;
129
+ progress?: number;
130
+ created_at?: string;
131
+ started_at?: string;
132
+ completed_at?: string;
133
+ scores?: Record<string, unknown>;
134
+ issues?: unknown;
135
+ error?: string;
136
+ }
137
+ /**
138
+ * Format audit result as SARIF 2.1.0 JSON.
139
+ *
140
+ * @param result - Audit result from API
141
+ * @param options - Formatting options
142
+ * @returns SARIF 2.1.0 JSON string
143
+ *
144
+ * @example
145
+ * ```typescript
146
+ * const sarif = formatSarif(auditResult, {
147
+ * workingDirectory: process.cwd(),
148
+ * includeBaseline: true,
149
+ * baselineFingerprints: new Set(['abc123...'])
150
+ * });
151
+ * ```
152
+ */
153
+ export declare function formatSarif(result: AuditResult, options?: SarifOptions): string;
154
+ /**
155
+ * Format options for the SARIF formatter (compatible with FormatOptions pattern).
156
+ */
157
+ export interface FormatSarifOptions {
158
+ sarif?: SarifOptions;
159
+ }
160
+ //# sourceMappingURL=sarif.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sarif.d.ts","sourceRoot":"","sources":["../../src/output/sarif.ts"],"names":[],"mappings":"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"}