@kb-labs/qa-core 2.93.0 → 2.96.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.
@@ -1,452 +1,222 @@
1
- import { getCheckIcon, getCheckLabel } from '@kb-labs/qa-contracts';
2
-
3
- // src/report/json-reporter.ts
4
- function buildJsonReport(results, diff, checks) {
5
- const severityMap = {};
6
- if (checks) {
7
- for (const c of checks) {
8
- severityMap[c.id] = c.severity ?? "blocker";
9
- }
10
- }
11
- const hasBlockerFailures = Object.entries(results).some(([ct, r]) => {
12
- const sev = severityMap[ct] ?? "blocker";
13
- return sev !== "info" && sev !== "warning" && r.failed.length > 0;
14
- });
15
- const summary = {};
16
- const failures = {};
17
- const errors = {};
18
- const blockers = [];
19
- const warnings = [];
20
- for (const ct of Object.keys(results)) {
21
- const r = results[ct];
22
- const total = r.passed.length + r.failed.length + r.skipped.length;
23
- summary[ct] = {
24
- total,
25
- passed: r.passed.length,
26
- failed: r.failed.length,
27
- skipped: r.skipped.length
28
- };
29
- failures[ct] = [...r.failed];
30
- errors[ct] = { ...r.errors };
31
- if (r.failed.length > 0) {
32
- const sev = severityMap[ct] ?? "blocker";
33
- const items = r.failed.flatMap((target) => {
34
- const detailItems = r.details?.[target];
35
- if (detailItems && detailItems.length > 0) {
36
- return detailItems;
37
- }
38
- const msg = r.errors[target];
39
- return msg ? [{ target, message: msg }] : [{ target, message: `check ${ct} failed` }];
40
- });
41
- if (sev === "blocker") {
42
- blockers.push({ check: ct, items });
43
- } else if (sev === "warning") {
44
- warnings.push({ check: ct, items });
45
- }
46
- }
47
- }
48
- return {
49
- status: hasBlockerFailures ? "failed" : "passed",
50
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
51
- summary,
52
- failures,
53
- errors,
54
- baseline: diff ?? null,
55
- blockers,
56
- warnings
57
- };
58
- }
59
- function buildDetailedJsonReport(results, grouped, diff, checks) {
60
- const base = buildJsonReport(results, diff, checks);
61
- return { ...base, grouped };
1
+ // src/report/text-reporter.ts
2
+ function formatTaskName(task) {
3
+ return task.split(/[-_]/).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
62
4
  }
63
- function icon(ct) {
64
- return getCheckIcon(ct);
65
- }
66
- function label(ct) {
67
- return getCheckLabel(ct);
68
- }
69
- function buildBaselineDiffLines(diff) {
70
- const lines = [];
71
- for (const ct of Object.keys(diff)) {
72
- const d = diff[ct];
73
- if (d.newFailures.length > 0) {
74
- lines.push(`${icon(ct)} ${label(ct)}: +${d.newFailures.length} new failures`);
75
- for (const pkg of d.newFailures) {
76
- lines.push(` - ${pkg}`);
77
- }
78
- }
79
- if (d.fixed.length > 0) {
80
- lines.push(`${icon(ct)} ${label(ct)}: -${d.fixed.length} fixed`);
81
- }
5
+ function buildRunReport(snap) {
6
+ if (!snap) {
7
+ return [{ header: "Run", lines: ["No run data available."] }];
82
8
  }
83
- return lines;
84
- }
85
- function buildRunReport(results, diff) {
86
9
  const sections = [];
87
- const summaryLines = [];
88
- let totalPassed = 0;
89
- let totalFailed = 0;
90
- let totalSkipped = 0;
91
- for (const ct of Object.keys(results)) {
92
- const r = results[ct];
93
- const total = r.passed.length + r.failed.length + r.skipped.length;
94
- const pct = total > 0 ? Math.round(r.passed.length / total * 100) : 100;
95
- const status = r.failed.length === 0 ? "PASS" : "FAIL";
96
- summaryLines.push(`${status} ${icon(ct)} ${label(ct).padEnd(12)} ${r.passed.length}/${total} passed (${pct}%)`);
97
- if (r.failed.length > 0) {
98
- for (const pkg of r.failed.slice(0, 5)) {
99
- summaryLines.push(` - ${pkg}`);
100
- }
101
- if (r.failed.length > 5) {
102
- summaryLines.push(` ... and ${r.failed.length - 5} more`);
10
+ const tasks = [...new Set(snap.raw.results.map((r) => r.Task))];
11
+ for (const task of tasks) {
12
+ const results = snap.raw.results.filter((r) => r.Task === task);
13
+ const passed = results.filter((r) => r.OK && !r.Cached).length;
14
+ const failed = results.filter((r) => !r.OK && !r.Cached).length;
15
+ const cached = results.filter((r) => r.Cached).length;
16
+ const failedPkgs = results.filter((r) => !r.OK && !r.Cached).map((r) => r.Package);
17
+ const lines = [`pass ${passed} fail ${failed} cached ${cached}`];
18
+ if (failedPkgs.length > 0) {
19
+ const shown = failedPkgs.slice(0, 5);
20
+ lines.push(...shown.map((p2) => ` \u2717 ${p2}`));
21
+ if (failedPkgs.length > 5) {
22
+ lines.push(` ... ${failedPkgs.length - 5} more`);
103
23
  }
104
24
  }
105
- totalPassed += r.passed.length;
106
- totalFailed += r.failed.length;
107
- totalSkipped += r.skipped.length;
108
- }
109
- sections.push({ header: "QA Summary Report", lines: summaryLines });
110
- if (diff) {
111
- const diffLines = buildBaselineDiffLines(diff);
112
- if (diffLines.length > 0) {
113
- sections.push({ header: "Baseline Comparison", lines: diffLines });
114
- }
25
+ sections.push({ header: formatTaskName(task), lines });
115
26
  }
27
+ const status = snap.raw.ok ? "passed" : "failed";
28
+ const { total, passed: p, failed: f, cached: c } = snap.raw.summary;
116
29
  sections.push({
117
- header: "Totals",
118
- lines: [`Total: ${totalPassed} passed, ${totalFailed} failed, ${totalSkipped} skipped`]
30
+ header: "Summary",
31
+ lines: [`${status.toUpperCase()} total ${total} pass ${p} fail ${f} cached ${c} (${snap.raw.elapsed})`]
119
32
  });
120
33
  return sections;
121
34
  }
122
- function buildHistoryTable(history, limit = 20) {
123
- const entries = history.slice(-limit);
124
- const lines = [];
125
- for (const entry of entries) {
126
- const date = new Date(entry.timestamp).toLocaleDateString();
127
- const status = entry.status === "passed" ? "PASS" : "FAIL";
128
- const summary = Object.keys(entry.summary).map((ct) => {
129
- const s = entry.summary[ct];
130
- return `${icon(ct)} ${s.failed}F`;
131
- }).join(" ");
132
- lines.push(`${date} ${entry.git.commit} ${status} ${summary} ${entry.git.message.slice(0, 40)}`);
35
+ function buildCheckReport(snap) {
36
+ if (!snap) {
37
+ return [{ header: "Check", lines: ["No check data available."] }];
133
38
  }
134
- return [{ header: `QA History (last ${entries.length})`, lines }];
135
- }
136
- function buildTrendsReport(trends, history) {
137
- if (trends.length === 0) {
138
- return [{ header: "QA Trends", lines: ["Not enough history (need at least 2 entries)"] }];
139
- }
140
- const lines = [];
141
- for (const t of trends) {
142
- const arrow = t.delta > 0 ? `+${t.delta} (regression)` : t.delta < 0 ? `${t.delta} (improvement)` : "\u2192 no change";
143
- lines.push(`${icon(t.checkType)} ${label(t.checkType).padEnd(12)} ${t.previous} \u2192 ${t.current} ${arrow}`);
39
+ const sections = [];
40
+ for (const [pkg, pkgData] of Object.entries(snap.raw.packages)) {
41
+ const issues = pkgData.issues ?? [];
42
+ if (issues.length === 0) {
43
+ continue;
44
+ }
45
+ const errors = issues.filter((i) => i.severity === "error").length;
46
+ const warnings = issues.filter((i) => i.severity === "warning").length;
47
+ const infos = issues.filter((i) => i.severity === "info").length;
48
+ const lines = [`errors ${errors} warnings ${warnings} info ${infos}`];
49
+ for (const issue of issues.slice(0, 5)) {
50
+ lines.push(` [${issue.severity}] ${issue.check}: ${issue.message}`);
51
+ }
52
+ if (issues.length > 5) {
53
+ lines.push(` ... ${issues.length - 5} more`);
54
+ }
55
+ sections.push({ header: pkg, lines });
144
56
  }
145
- if (history.length >= 2) {
146
- const first = history[Math.max(0, history.length - 10)];
147
- const last = history[history.length - 1];
148
- lines.push("");
149
- lines.push(`Period: ${new Date(first.timestamp).toLocaleDateString()} \u2192 ${new Date(last.timestamp).toLocaleDateString()}`);
57
+ if (sections.length === 0) {
58
+ sections.push({ header: "Check", lines: ["All packages OK."] });
150
59
  }
151
- return [{ header: "QA Trends", lines }];
60
+ return sections;
152
61
  }
153
- function buildRegressionsReport(result, history) {
154
- if (history.length < 2) {
155
- return [{ header: "Regression Detection", lines: ["Not enough history (need at least 2 entries)"] }];
62
+ function buildStatsReport(snap) {
63
+ if (!snap) {
64
+ return [{ header: "Stats", lines: ["No stats data available."] }];
156
65
  }
157
- const prev = history[history.length - 2];
158
- const curr = history[history.length - 1];
159
- const lines = [
160
- `Comparing: ${prev.git.commit} \u2192 ${curr.git.commit}`,
161
- ""
162
- ];
163
- if (!result.hasRegressions) {
164
- lines.push("No regressions detected.");
165
- return [{ header: "Regression Detection", lines }];
166
- }
167
- for (const r of result.regressions) {
168
- lines.push(`${r.checkType}: +${r.newFailures.length} new failures`);
169
- for (const pkg of r.newFailures) {
170
- lines.push(` - ${pkg}`);
171
- }
66
+ const { score, grade, summary, by_category, coverage } = snap.raw;
67
+ const sections = [];
68
+ sections.push({
69
+ header: "Score",
70
+ lines: [`${score}/100 Grade: ${grade} (${summary.healthy} healthy / ${summary.warning} warning / ${summary.error} error)`]
71
+ });
72
+ const catLines = Object.entries(by_category).map(([cat, data]) => {
73
+ const pct = data.total > 0 ? Math.round(data.healthy / data.total * 100) : 0;
74
+ return ` ${cat.padEnd(20)} ${data.grade} ${pct}% (${data.healthy}/${data.total})`;
75
+ });
76
+ sections.push({ header: "By Category", lines: catLines });
77
+ if (Object.keys(coverage).length > 0) {
78
+ const covLines = Object.entries(coverage).map(([k, v]) => ` ${k.padEnd(20)} ${v.pct}% (${v.pass}/${v.total})`);
79
+ sections.push({ header: "Coverage", lines: covLines });
172
80
  }
173
- lines.push("");
174
- lines.push("REGRESSIONS DETECTED!");
175
- return [{ header: "Regression Detection", lines }];
81
+ return sections;
176
82
  }
177
- function buildBaselineReport(baseline) {
178
- if (!baseline) {
179
- return [{ header: "Baseline Status", lines: ["No baseline captured yet. Run baseline:update first."] }];
83
+ function buildHistoryTable(history, limit = 20) {
84
+ const rows = [...history].reverse().slice(0, limit);
85
+ if (rows.length === 0) {
86
+ return [{ header: "History", lines: ["No history available."] }];
87
+ }
88
+ const lines = rows.map((snap) => {
89
+ const status = snap.raw.ok ? "pass" : "fail";
90
+ const tasks = snap.tasks.join(", ");
91
+ const commit = snap.git?.commit ?? "-------";
92
+ const date = new Date(snap.timestamp).toLocaleString();
93
+ return ` ${date} ${commit} ${status} [${tasks}]`;
94
+ });
95
+ return [{ header: `History (last ${rows.length})`, lines }];
96
+ }
97
+ function buildTrendsReport(analysis) {
98
+ if (analysis.tasks.length === 0) {
99
+ return [{ header: "Trends", lines: ["Not enough history for trend analysis."] }];
180
100
  }
181
- const lines = [
182
- `Captured: ${new Date(baseline.timestamp).toLocaleString()}`,
183
- `Git: ${baseline.git.commit} (${baseline.git.branch})`,
184
- ""
185
- ];
186
- for (const ct of Object.keys(baseline.results)) {
187
- const r = baseline.results[ct];
188
- lines.push(`${icon(ct)} ${label(ct).padEnd(12)} ${r.passed} passed, ${r.failed} failed`);
189
- if (r.failedPackages.length > 0) {
190
- const shown = r.failedPackages.slice(0, 3);
191
- for (const pkg of shown) {
192
- lines.push(` - ${pkg}`);
101
+ const sections = [];
102
+ for (const trend of analysis.tasks) {
103
+ const arrow = trend.direction === "regression" ? "\u2191" : trend.direction === "improvement" ? "\u2193" : "\u2192";
104
+ const lines = [
105
+ `${arrow} ${trend.direction} prev ${trend.previous} curr ${trend.current} delta ${trend.delta > 0 ? "+" : ""}${trend.delta} velocity ${trend.velocity}`
106
+ ];
107
+ if (trend.changelog.length > 0) {
108
+ const last = trend.changelog[trend.changelog.length - 1];
109
+ if (last.newFailures.length > 0) {
110
+ lines.push(` last regression: ${last.newFailures.slice(0, 3).join(", ")}`);
193
111
  }
194
- if (r.failedPackages.length > 3) {
195
- lines.push(` ... and ${r.failedPackages.length - 3} more`);
112
+ if (last.fixed.length > 0) {
113
+ lines.push(` last fixed: ${last.fixed.slice(0, 3).join(", ")}`);
196
114
  }
197
115
  }
116
+ sections.push({ header: formatTaskName(trend.task), lines });
198
117
  }
199
- return [{ header: "Baseline Status", lines }];
200
- }
201
- function checkTag(status, ct) {
202
- const short = ct === "typeCheck" ? "types" : ct;
203
- if (status === "failed") {
204
- return short.toUpperCase();
205
- }
206
- if (status === "skipped") {
207
- return `-${short}-`;
208
- }
209
- return short;
210
- }
211
- function getErrorPreview(raw) {
212
- const errLines = raw.split("\n").filter((l) => l.trim().length > 0);
213
- for (const el of errLines) {
214
- const cleaned = el.replace(/^Command failed: .*/, "").trim();
215
- if (cleaned.length > 0) {
216
- return cleaned.replace(/\/[^\s]*\/kb-labs\//g, "").slice(0, 100);
217
- }
218
- }
219
- return "";
118
+ return sections;
220
119
  }
221
- function renderPackageLines(pkg, lines) {
222
- const hasFail = Object.values(pkg.checks).some((v) => v === "failed");
223
- const status = hasFail ? "FAIL" : "PASS";
224
- const tags = Object.keys(pkg.checks).map((ct) => checkTag(pkg.checks[ct], ct)).join(" ");
225
- lines.push(` ${status} ${pkg.name.padEnd(40)} ${tags}`);
226
- if (hasFail) {
227
- for (const ct of Object.keys(pkg.checks)) {
228
- if (pkg.checks[ct] === "failed") {
229
- const preview = getErrorPreview((pkg.errors[ct] ?? "").trim());
230
- lines.push(` ${ct}: ${preview || "failed"}`);
231
- }
232
- }
120
+ function buildRegressionsReport(detection) {
121
+ if (!detection.hasRegressions) {
122
+ return [{ header: "Regressions", lines: ["No regressions detected."] }];
233
123
  }
124
+ const sections = [];
125
+ for (const reg of detection.regressions) {
126
+ const lines = [
127
+ `delta +${reg.delta}`,
128
+ ...reg.newFailures.slice(0, 5).map((p) => ` \u2717 ${p}`)
129
+ ];
130
+ if (reg.newFailures.length > 5) {
131
+ lines.push(` ... ${reg.newFailures.length - 5} more`);
132
+ }
133
+ sections.push({ header: formatTaskName(reg.task), lines });
134
+ }
135
+ const prev = new Date(detection.comparedAt.previous).toLocaleString();
136
+ const curr = new Date(detection.comparedAt.current).toLocaleString();
137
+ sections.push({ header: "Compared", lines: [`${prev} \u2192 ${curr}`] });
138
+ return sections;
234
139
  }
235
- function renderCategoryLines(catKey, grouped) {
236
- const cat = grouped.categories[catKey];
237
- const lines = [`PASS ${cat.summary.passed} | FAIL ${cat.summary.failed}`, ""];
238
- for (const repoKey of Object.keys(cat.repos).sort()) {
239
- const repo = cat.repos[repoKey];
240
- lines.push(` ${repoKey} (${repo.summary.total} packages)`);
241
- const sorted = [...repo.packages].sort((a, b) => {
242
- const aFail = Object.values(a.checks).some((v) => v === "failed") ? 0 : 1;
243
- const bFail = Object.values(b.checks).some((v) => v === "failed") ? 0 : 1;
244
- if (aFail !== bFail) {
245
- return aFail - bFail;
246
- }
247
- return a.name.localeCompare(b.name);
248
- });
249
- for (const pkg of sorted) {
250
- renderPackageLines(pkg, lines);
251
- }
252
- lines.push("");
253
- }
254
- return lines;
255
- }
256
- function buildDetailedRunReport(grouped, diff) {
140
+ function buildBaselineReport(baseline) {
141
+ if (!baseline) {
142
+ return [{ header: "Baseline", lines: ["No baseline set. Run `qa baseline update` to set one."] }];
143
+ }
144
+ const { score, grade } = baseline.stats;
145
+ const totalIssues = Object.values(baseline.check.packages).reduce((sum, pkg) => sum + (pkg.issues?.length ?? 0), 0);
146
+ const commit = baseline.git?.commit ?? "unknown";
147
+ const date = new Date(baseline.timestamp).toLocaleString();
148
+ return [{
149
+ header: "Baseline",
150
+ lines: [`score ${score}/100 grade ${grade} issues ${totalIssues} commit ${commit} set ${date}`]
151
+ }];
152
+ }
153
+ function buildBaselineDiffReport(diff) {
257
154
  const sections = [];
258
- const categoryKeys = Object.keys(grouped.categories).sort((a, b) => {
259
- if (a === "uncategorized") {
260
- return 1;
261
- }
262
- if (b === "uncategorized") {
263
- return -1;
264
- }
265
- return a.localeCompare(b);
155
+ const scoreDir = diff.scoreDelta > 0 ? "+" : "";
156
+ sections.push({
157
+ header: "Score Delta",
158
+ lines: [`${scoreDir}${diff.scoreDelta} ${diff.gradeDelta} new ${diff.newIssueCount} fixed ${diff.fixedIssueCount}`]
266
159
  });
267
- for (const catKey of categoryKeys) {
268
- const cat = grouped.categories[catKey];
269
- sections.push({ header: `${cat.label} (${cat.summary.total} packages)`, lines: renderCategoryLines(catKey, grouped) });
160
+ if (diff.newIssues.length > 0) {
161
+ const lines = diff.newIssues.slice(0, 10).map((i) => ` [${i.severity}] ${i.pkg} ${i.check}: ${i.message}`);
162
+ if (diff.newIssues.length > 10) {
163
+ lines.push(` ... ${diff.newIssues.length - 10} more`);
164
+ }
165
+ sections.push({ header: "New Issues", lines });
270
166
  }
271
- if (diff) {
272
- const diffLines = buildBaselineDiffLines(diff);
273
- if (diffLines.length > 0) {
274
- sections.push({ header: "Baseline Comparison", lines: diffLines });
167
+ if (diff.fixedIssues.length > 0) {
168
+ const lines = diff.fixedIssues.slice(0, 10).map((i) => ` [${i.severity}] ${i.pkg} ${i.check}: ${i.message}`);
169
+ if (diff.fixedIssues.length > 10) {
170
+ lines.push(` ... ${diff.fixedIssues.length - 10} more`);
275
171
  }
172
+ sections.push({ header: "Fixed Issues", lines });
276
173
  }
277
- let totalPassed = 0;
278
- let totalFailed = 0;
279
- for (const catKey of categoryKeys) {
280
- totalPassed += grouped.categories[catKey].summary.passed;
281
- totalFailed += grouped.categories[catKey].summary.failed;
174
+ if (diff.persistingIssues.length > 0) {
175
+ sections.push({ header: "Persisting Issues", lines: [`${diff.persistingIssues.length} issues unchanged`] });
282
176
  }
283
- sections.push({
284
- header: "Totals",
285
- lines: [`Total: ${totalPassed} passed, ${totalFailed} failed (${categoryKeys.length} categories)`]
286
- });
287
177
  return sections;
288
178
  }
289
179
 
290
- // src/report/grouped-reporter.ts
291
- function emptyGroupSummary(checkTypes) {
292
- const checks = {};
293
- for (const ct of checkTypes) {
294
- checks[ct] = { passed: 0, failed: 0, skipped: 0 };
295
- }
296
- return { total: 0, passed: 0, failed: 0, checks };
297
- }
298
- function resolveCheckStatus(pkgName, ct, results) {
299
- const r = results[ct];
300
- if (!r) {
301
- return "skipped";
302
- }
303
- if (r.failed.includes(pkgName)) {
304
- return "failed";
305
- }
306
- if (r.passed.includes(pkgName)) {
307
- return "passed";
308
- }
309
- return "skipped";
310
- }
311
- function buildPackageStatus(pkg, results, category) {
312
- const checks = {};
313
- const errors = {};
314
- for (const ct of Object.keys(results)) {
315
- checks[ct] = resolveCheckStatus(pkg.name, ct, results);
316
- if (checks[ct] === "failed" && results[ct]?.errors[pkg.name]) {
317
- errors[ct] = results[ct].errors[pkg.name];
318
- }
319
- }
180
+ // src/report/json-reporter.ts
181
+ function buildRunJsonReport(snap) {
320
182
  return {
321
- name: pkg.name,
322
- repo: pkg.repo,
323
- category,
324
- checks,
325
- errors
183
+ id: snap.id,
184
+ timestamp: snap.timestamp,
185
+ git: snap.git,
186
+ durationMs: snap.durationMs,
187
+ tasks: snap.tasks,
188
+ ok: snap.raw.ok,
189
+ elapsed: snap.raw.elapsed,
190
+ summary: snap.raw.summary,
191
+ results: snap.raw.results
326
192
  };
327
193
  }
328
- function addToSummary(summary, status) {
329
- summary.total++;
330
- const hasFail = Object.values(status.checks).some((v) => v === "failed");
331
- if (hasFail) {
332
- summary.failed++;
333
- } else {
334
- summary.passed++;
335
- }
336
- for (const ct of Object.keys(status.checks)) {
337
- const s = status.checks[ct];
338
- if (!summary.checks[ct]) {
339
- summary.checks[ct] = { passed: 0, failed: 0, skipped: 0 };
340
- }
341
- if (s === "passed") {
342
- summary.checks[ct].passed++;
343
- } else if (s === "failed") {
344
- summary.checks[ct].failed++;
345
- } else {
346
- summary.checks[ct].skipped++;
347
- }
348
- }
349
- }
350
- function groupResults(results, packages, categoryMap, config) {
351
- const checkTypes = Object.keys(results);
352
- const grouped = { categories: {} };
353
- for (const pkg of packages) {
354
- const categoryKeys = categoryMap.get(pkg.name) ?? ["uncategorized"];
355
- for (const categoryKey of categoryKeys) {
356
- const status = buildPackageStatus(pkg, results, categoryKey);
357
- if (!grouped.categories[categoryKey]) {
358
- const label2 = categoryKey === "uncategorized" ? "Uncategorized" : config?.categories?.[categoryKey]?.label ?? categoryKey;
359
- grouped.categories[categoryKey] = {
360
- label: label2,
361
- repos: {},
362
- summary: emptyGroupSummary(checkTypes)
363
- };
364
- }
365
- const categoryGroup = grouped.categories[categoryKey];
366
- if (!categoryGroup.repos[pkg.repo]) {
367
- categoryGroup.repos[pkg.repo] = {
368
- packages: [],
369
- summary: emptyGroupSummary(checkTypes)
370
- };
371
- }
372
- const repoGroup = categoryGroup.repos[pkg.repo];
373
- repoGroup.packages.push(status);
374
- addToSummary(repoGroup.summary, status);
375
- addToSummary(categoryGroup.summary, status);
376
- }
377
- }
378
- return grouped;
379
- }
380
-
381
- // src/report/error-grouping.ts
382
- function extractPattern(errorText, checkType) {
383
- if (checkType === "lint") {
384
- const ruleMatch = errorText.match(/(\S+\/[\w-]+|no-[\w-]+)/);
385
- if (ruleMatch?.[1]) {
386
- return ruleMatch[1];
387
- }
388
- }
389
- if (checkType === "typeCheck") {
390
- const tsMatch = errorText.match(/TS(\d{4,5})/);
391
- if (tsMatch) {
392
- return `TS${tsMatch[1]}`;
393
- }
394
- }
395
- if (checkType === "test") {
396
- const failMatch = errorText.match(/FAIL\s+(\S+)/);
397
- if (failMatch) {
398
- return `FAIL: ${failMatch[1]}`;
399
- }
400
- }
401
- if (checkType === "build") {
402
- if (errorText.includes("Cannot find module")) {
403
- return "Cannot find module";
404
- }
405
- if (errorText.includes("Module not found")) {
406
- return "Module not found";
407
- }
408
- }
409
- const firstLine = errorText.split("\n").find((l) => l.trim().length > 0)?.trim() ?? "";
410
- return firstLine.slice(0, 100) || "Unknown error";
194
+ function buildCheckJsonReport(snap) {
195
+ return {
196
+ id: snap.id,
197
+ timestamp: snap.timestamp,
198
+ git: snap.git,
199
+ durationMs: snap.durationMs,
200
+ ok: snap.raw.ok,
201
+ packages: snap.raw.packages
202
+ };
411
203
  }
412
- function groupErrors(results) {
413
- const groupMap = /* @__PURE__ */ new Map();
414
- let ungrouped = 0;
415
- for (const ct of Object.keys(results)) {
416
- const check = results[ct];
417
- if (!check.errors) {
418
- continue;
419
- }
420
- for (const [pkgName, errorText] of Object.entries(check.errors)) {
421
- const pattern = extractPattern(errorText, ct);
422
- const key = `${ct}::${pattern}`;
423
- const existing = groupMap.get(key);
424
- if (existing) {
425
- existing.count++;
426
- existing.packages.push(pkgName);
427
- } else {
428
- groupMap.set(key, {
429
- pattern,
430
- count: 1,
431
- packages: [pkgName],
432
- checkType: ct,
433
- example: errorText.slice(0, 200)
434
- });
435
- }
436
- }
437
- }
438
- const groups = [];
439
- for (const group of groupMap.values()) {
440
- if (group.count === 1) {
441
- ungrouped++;
442
- } else {
443
- groups.push(group);
444
- }
445
- }
446
- groups.sort((a, b) => b.count - a.count);
447
- return { groups, ungrouped };
204
+ function buildStatsJsonReport(snap) {
205
+ return {
206
+ id: snap.id,
207
+ timestamp: snap.timestamp,
208
+ git: snap.git,
209
+ durationMs: snap.durationMs,
210
+ ok: snap.raw.ok,
211
+ score: snap.raw.score,
212
+ grade: snap.raw.grade,
213
+ summary: snap.raw.summary,
214
+ by_category: snap.raw.by_category,
215
+ issues_by_type: snap.raw.issues_by_type,
216
+ coverage: snap.raw.coverage
217
+ };
448
218
  }
449
219
 
450
- export { buildBaselineReport, buildDetailedJsonReport, buildDetailedRunReport, buildHistoryTable, buildJsonReport, buildRegressionsReport, buildRunReport, buildTrendsReport, groupErrors, groupResults };
220
+ export { buildBaselineDiffReport, buildBaselineReport, buildCheckJsonReport, buildCheckReport, buildHistoryTable, buildRegressionsReport, buildRunJsonReport, buildRunReport, buildStatsJsonReport, buildStatsReport, buildTrendsReport, formatTaskName };
451
221
  //# sourceMappingURL=index.js.map
452
222
  //# sourceMappingURL=index.js.map