@kb-labs/qa-core 0.6.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/dist/index.js ADDED
@@ -0,0 +1,1550 @@
1
+ import { existsSync, readFileSync, mkdirSync, writeFileSync, readdirSync, statSync } from 'fs';
2
+ import { join, relative, dirname, resolve } from 'path';
3
+ import { execSync, spawnSync } from 'child_process';
4
+ import { createHash } from 'crypto';
5
+ import { TRENDS_WINDOW, PATHS, HISTORY_MAX_ENTRIES, getCheckIcon, getCheckLabel } from '@kb-labs/qa-contracts';
6
+
7
+ // src/runner/workspace.ts
8
+ function getSubmoduleInfo(repoDir, repoName) {
9
+ const gitDir = join(repoDir, ".git");
10
+ if (!existsSync(gitDir)) {
11
+ return null;
12
+ }
13
+ try {
14
+ const commit = execSync("git rev-parse --short HEAD", {
15
+ cwd: repoDir,
16
+ encoding: "utf-8",
17
+ timeout: 5e3,
18
+ stdio: ["pipe", "pipe", "pipe"]
19
+ }).trim();
20
+ const branch = execSync("git rev-parse --abbrev-ref HEAD", {
21
+ cwd: repoDir,
22
+ encoding: "utf-8",
23
+ timeout: 5e3,
24
+ stdio: ["pipe", "pipe", "pipe"]
25
+ }).trim();
26
+ const message = execSync("git log -1 --format=%s", {
27
+ cwd: repoDir,
28
+ encoding: "utf-8",
29
+ timeout: 5e3,
30
+ stdio: ["pipe", "pipe", "pipe"]
31
+ }).trim();
32
+ const statusOutput = execSync("git status --porcelain", {
33
+ cwd: repoDir,
34
+ encoding: "utf-8",
35
+ timeout: 5e3,
36
+ stdio: ["pipe", "pipe", "pipe"]
37
+ }).trim();
38
+ return {
39
+ name: repoName,
40
+ commit,
41
+ branch,
42
+ dirty: statusOutput.length > 0,
43
+ message
44
+ };
45
+ } catch {
46
+ return null;
47
+ }
48
+ }
49
+ function collectSubmoduleInfo(rootDir, repos) {
50
+ const result = {};
51
+ for (const repo of repos) {
52
+ const repoDir = join(rootDir, repo);
53
+ const info = getSubmoduleInfo(repoDir, repo);
54
+ if (info) {
55
+ result[repo] = info;
56
+ }
57
+ }
58
+ return result;
59
+ }
60
+
61
+ // src/runner/workspace.ts
62
+ function hasWorkspace(dir) {
63
+ return existsSync(join(dir, "pnpm-workspace.yaml"));
64
+ }
65
+ function isDir(p) {
66
+ return existsSync(p) && statSync(p).isDirectory();
67
+ }
68
+ function buildCandidatesFromConfig(rootDir, paths) {
69
+ const candidates = [];
70
+ for (const pattern of paths) {
71
+ const parts = pattern.split("/");
72
+ if (parts.length === 2 && parts[1] === "*" && parts[0]) {
73
+ const categoryDir = join(rootDir, parts[0]);
74
+ if (!isDir(categoryDir)) {
75
+ continue;
76
+ }
77
+ try {
78
+ for (const sub of readdirSync(categoryDir)) {
79
+ if (sub.startsWith(".") || sub === "node_modules") {
80
+ continue;
81
+ }
82
+ const subPath = join(categoryDir, sub);
83
+ if (isDir(subPath) && hasWorkspace(subPath)) {
84
+ candidates.push(subPath);
85
+ }
86
+ }
87
+ } catch {
88
+ }
89
+ } else {
90
+ const exactPath = join(rootDir, pattern);
91
+ if (isDir(exactPath) && hasWorkspace(exactPath)) {
92
+ candidates.push(exactPath);
93
+ }
94
+ }
95
+ }
96
+ return candidates;
97
+ }
98
+ function buildCandidatesAutoScan(rootDir) {
99
+ const candidates = [];
100
+ for (const entry of readdirSync(rootDir)) {
101
+ if (entry.startsWith(".") || entry === "node_modules" || entry === "dist") {
102
+ continue;
103
+ }
104
+ const entryPath = join(rootDir, entry);
105
+ if (!isDir(entryPath)) {
106
+ continue;
107
+ }
108
+ if (hasWorkspace(entryPath)) {
109
+ candidates.push(entryPath);
110
+ } else {
111
+ try {
112
+ for (const sub of readdirSync(entryPath)) {
113
+ if (sub.startsWith(".") || sub === "node_modules") {
114
+ continue;
115
+ }
116
+ const subPath = join(entryPath, sub);
117
+ if (isDir(subPath) && hasWorkspace(subPath)) {
118
+ candidates.push(subPath);
119
+ }
120
+ }
121
+ } catch {
122
+ }
123
+ }
124
+ }
125
+ return candidates;
126
+ }
127
+ function scanSubDir(parentDir, entryPath, repoName, rootDir, submodule, packages) {
128
+ if (!isDir(parentDir)) {
129
+ return;
130
+ }
131
+ for (const pkgDir of readdirSync(parentDir)) {
132
+ const pkgPath = join(parentDir, pkgDir);
133
+ const pkgJsonPath = join(pkgPath, "package.json");
134
+ if (!existsSync(pkgJsonPath)) {
135
+ continue;
136
+ }
137
+ try {
138
+ const pkgJson = JSON.parse(readFileSync(pkgJsonPath, "utf-8"));
139
+ packages.push({
140
+ name: pkgJson.name || pkgDir,
141
+ dir: pkgPath,
142
+ relativePath: relative(rootDir, pkgPath),
143
+ repo: repoName,
144
+ submodule
145
+ });
146
+ } catch {
147
+ }
148
+ }
149
+ }
150
+ function getWorkspacePackages(rootDir, filter, packagesConfig) {
151
+ const packages = [];
152
+ const submoduleCache = /* @__PURE__ */ new Map();
153
+ function getSubmoduleCached(entryPath, repoName) {
154
+ if (!submoduleCache.has(repoName)) {
155
+ submoduleCache.set(repoName, getSubmoduleInfo(entryPath, repoName));
156
+ }
157
+ return submoduleCache.get(repoName) ?? void 0;
158
+ }
159
+ const candidates = packagesConfig?.paths && packagesConfig.paths.length > 0 ? buildCandidatesFromConfig(rootDir, packagesConfig.paths) : buildCandidatesAutoScan(rootDir);
160
+ for (const entryPath of candidates) {
161
+ const repoName = relative(rootDir, entryPath);
162
+ const submodule = getSubmoduleCached(entryPath, repoName);
163
+ scanSubDir(join(entryPath, "packages"), entryPath, repoName, rootDir, submodule, packages);
164
+ scanSubDir(join(entryPath, "apps"), entryPath, repoName, rootDir, submodule, packages);
165
+ }
166
+ let filtered = packages;
167
+ if (packagesConfig?.include && packagesConfig.include.length > 0) {
168
+ filtered = filtered.filter(
169
+ (pkg) => packagesConfig.include.some((pattern) => matchesPattern(pkg.name, pkg.repo, pattern))
170
+ );
171
+ }
172
+ if (packagesConfig?.exclude && packagesConfig.exclude.length > 0) {
173
+ filtered = filtered.filter(
174
+ (pkg) => !packagesConfig.exclude.some((pattern) => matchesPattern(pkg.name, pkg.repo, pattern))
175
+ );
176
+ }
177
+ if (!filter) {
178
+ return filtered;
179
+ }
180
+ return filtered.filter((pkg) => {
181
+ if (filter.package && !pkg.name.includes(filter.package)) {
182
+ return false;
183
+ }
184
+ if (filter.repo && pkg.repo !== filter.repo) {
185
+ return false;
186
+ }
187
+ if (filter.scope) {
188
+ const scope = filter.scope.startsWith("@") ? filter.scope : `@${filter.scope}`;
189
+ if (!pkg.name.startsWith(scope)) {
190
+ return false;
191
+ }
192
+ }
193
+ return true;
194
+ });
195
+ }
196
+ function matchesPattern(name, repo, pattern) {
197
+ if (pattern.endsWith("/*")) {
198
+ const prefix = pattern.slice(0, -2);
199
+ return repo === prefix || repo.startsWith(prefix + "/");
200
+ }
201
+ if (pattern.endsWith("*")) {
202
+ return name.startsWith(pattern.slice(0, -1));
203
+ }
204
+ return name === pattern || repo === pattern;
205
+ }
206
+ function readWorkspaceDeps(pkgDir, workspaceNames) {
207
+ try {
208
+ const raw = readFileSync(join(pkgDir, "package.json"), "utf-8");
209
+ const pkgJson = JSON.parse(raw);
210
+ const allDeps = { ...pkgJson.dependencies, ...pkgJson.devDependencies };
211
+ return Object.keys(allDeps).filter((dep) => workspaceNames.has(dep));
212
+ } catch {
213
+ return [];
214
+ }
215
+ }
216
+ function buildDepGraph(packages) {
217
+ const nameMap = /* @__PURE__ */ new Map();
218
+ const workspaceNames = /* @__PURE__ */ new Set();
219
+ for (const pkg of packages) {
220
+ nameMap.set(pkg.name, pkg);
221
+ workspaceNames.add(pkg.name);
222
+ }
223
+ const inDegree = /* @__PURE__ */ new Map();
224
+ const dependents = /* @__PURE__ */ new Map();
225
+ for (const pkg of packages) {
226
+ inDegree.set(pkg.name, 0);
227
+ dependents.set(pkg.name, []);
228
+ }
229
+ for (const pkg of packages) {
230
+ const deps = readWorkspaceDeps(pkg.dir, workspaceNames);
231
+ inDegree.set(pkg.name, deps.length);
232
+ for (const dep of deps) {
233
+ dependents.get(dep).push(pkg.name);
234
+ }
235
+ }
236
+ return { nameMap, inDegree, dependents };
237
+ }
238
+ function computeBuildLayers(packages) {
239
+ if (packages.length === 0) {
240
+ return [];
241
+ }
242
+ const { nameMap, inDegree, dependents } = buildDepGraph(packages);
243
+ const layers = [];
244
+ const remaining = new Set(packages.map((p) => p.name));
245
+ while (remaining.size > 0) {
246
+ const layerNames = [];
247
+ for (const name of remaining) {
248
+ if ((inDegree.get(name) ?? 0) === 0) {
249
+ layerNames.push(name);
250
+ }
251
+ }
252
+ if (layerNames.length === 0) {
253
+ const circular = [...remaining].map((n) => nameMap.get(n)).filter(Boolean);
254
+ layers.push({ index: layers.length, packages: circular });
255
+ break;
256
+ }
257
+ layerNames.sort();
258
+ layers.push({ index: layers.length, packages: layerNames.map((n) => nameMap.get(n)) });
259
+ for (const name of layerNames) {
260
+ remaining.delete(name);
261
+ for (const dependent of dependents.get(name) ?? []) {
262
+ inDegree.set(dependent, (inDegree.get(dependent) ?? 0) - 1);
263
+ }
264
+ }
265
+ }
266
+ return layers;
267
+ }
268
+ function sortByBuildLayers(packages) {
269
+ return computeBuildLayers(packages).flatMap((l) => l.packages);
270
+ }
271
+
272
+ // src/runner/custom-check-runner.ts
273
+ var ID_MAP = {
274
+ build: "build",
275
+ lint: "lint",
276
+ typecheck: "typeCheck",
277
+ "type-check": "typeCheck",
278
+ test: "test",
279
+ tests: "test"
280
+ };
281
+ function emptyResult() {
282
+ return { passed: [], failed: [], skipped: [], errors: {} };
283
+ }
284
+ function runCommand(command, args, cwd, timeoutMs) {
285
+ try {
286
+ const result = spawnSync(command, args, { cwd, timeout: timeoutMs, encoding: "utf-8", shell: false });
287
+ const stdout = result.stdout ?? "";
288
+ const stderr = result.stderr ?? "";
289
+ const exitCode = result.status ?? 1;
290
+ return { ok: exitCode === 0, stdout, stderr, exitCode };
291
+ } catch (e) {
292
+ return { ok: false, stdout: "", stderr: e.message ?? String(e), exitCode: 1 };
293
+ }
294
+ }
295
+ function evaluate(check, stdout, stderr, exitCode) {
296
+ if ((check.parser ?? "exitcode") === "json") {
297
+ try {
298
+ const parsed = JSON.parse(stdout);
299
+ return parsed.ok === true || parsed.success === true || parsed.status === "ok";
300
+ } catch {
301
+ return false;
302
+ }
303
+ }
304
+ return exitCode === 0;
305
+ }
306
+ function recordResult(bucket, key, passed, stdout, stderr, exitCode, canonicalId, onProgress, durationMs) {
307
+ if (passed) {
308
+ bucket.passed.push(key);
309
+ onProgress?.(canonicalId, key, "pass", durationMs);
310
+ } else {
311
+ bucket.failed.push(key);
312
+ bucket.errors[key] = stderr || stdout || `Exit code ${exitCode}`;
313
+ onProgress?.(canonicalId, key, "fail", durationMs);
314
+ }
315
+ }
316
+ function runInRepoRoot(check, canonicalId, resolvedArgs, rootDir, bucket, onProgress) {
317
+ const startMs = Date.now();
318
+ const { stderr, exitCode, stdout } = runCommand(check.command, resolvedArgs, rootDir, check.timeoutMs ?? 12e4);
319
+ recordResult(bucket, rootDir, evaluate(check, stdout, stderr, exitCode), stdout, stderr, exitCode, canonicalId, onProgress, Date.now() - startMs);
320
+ }
321
+ function runInScopePath(check, canonicalId, resolvedArgs, packages, rootDir, bucket, onProgress) {
322
+ const seen = /* @__PURE__ */ new Set();
323
+ for (const pkg of packages) {
324
+ if (seen.has(pkg.repo)) {
325
+ continue;
326
+ }
327
+ seen.add(pkg.repo);
328
+ const scopeDir = resolve(rootDir, pkg.repo);
329
+ if (!existsSync(scopeDir)) {
330
+ continue;
331
+ }
332
+ const startMs = Date.now();
333
+ const { stderr, exitCode, stdout } = runCommand(check.command, resolvedArgs, scopeDir, check.timeoutMs ?? 12e4);
334
+ recordResult(bucket, pkg.repo, evaluate(check, stdout, stderr, exitCode), stdout, stderr, exitCode, canonicalId, onProgress, Date.now() - startMs);
335
+ }
336
+ }
337
+ function getPnpmScriptName(check) {
338
+ if (check.command !== "pnpm") {
339
+ return void 0;
340
+ }
341
+ const args = check.args ?? [];
342
+ if (args[0] === "run" && args[1]) {
343
+ return args[1];
344
+ }
345
+ return void 0;
346
+ }
347
+ function hasNpmScript(pkgDir, scriptName) {
348
+ try {
349
+ const pkgJson = JSON.parse(readFileSync(join(pkgDir, "package.json"), "utf-8"));
350
+ return typeof pkgJson?.scripts?.[scriptName] === "string";
351
+ } catch {
352
+ return false;
353
+ }
354
+ }
355
+ function runPerPackage(check, canonicalId, resolvedArgs, packages, bucket, onProgress) {
356
+ const sortedPackages = check.ordered ? sortByBuildLayers(packages) : packages;
357
+ const scriptName = getPnpmScriptName(check);
358
+ for (const pkg of sortedPackages) {
359
+ if (scriptName && !hasNpmScript(pkg.dir, scriptName)) {
360
+ bucket.skipped.push(pkg.name);
361
+ onProgress?.(canonicalId, pkg.name, "skip");
362
+ continue;
363
+ }
364
+ const startMs = Date.now();
365
+ const { stderr, exitCode, stdout } = runCommand(check.command, resolvedArgs, pkg.dir, check.timeoutMs ?? 12e4);
366
+ recordResult(bucket, pkg.name, evaluate(check, stdout, stderr, exitCode), stdout, stderr, exitCode, canonicalId, onProgress, Date.now() - startMs);
367
+ }
368
+ }
369
+ function runCustomChecks(checks, packages, rootDir, onProgress) {
370
+ const results = {};
371
+ for (const check of checks) {
372
+ const canonicalId = ID_MAP[check.id.toLowerCase()] ?? check.id;
373
+ if (!results[canonicalId]) {
374
+ results[canonicalId] = emptyResult();
375
+ }
376
+ const bucket = results[canonicalId];
377
+ const args = check.args ?? [];
378
+ const resolvedArgs = args.map(
379
+ (arg) => arg.match(/\.(sh|js|ts|mjs|cjs)$/) && !arg.startsWith("/") ? join(rootDir, arg) : arg
380
+ );
381
+ const runIn = check.runIn ?? "perPackage";
382
+ if (runIn === "repoRoot") {
383
+ runInRepoRoot(check, canonicalId, resolvedArgs, rootDir, bucket, onProgress);
384
+ } else if (runIn === "scopePath") {
385
+ runInScopePath(check, canonicalId, resolvedArgs, packages, rootDir, bucket, onProgress);
386
+ } else {
387
+ runPerPackage(check, canonicalId, resolvedArgs, packages, bucket, onProgress);
388
+ }
389
+ }
390
+ return results;
391
+ }
392
+ function loadCache(rootDir) {
393
+ const cachePath = join(rootDir, PATHS.CACHE);
394
+ if (!existsSync(cachePath)) {
395
+ return {};
396
+ }
397
+ try {
398
+ return JSON.parse(readFileSync(cachePath, "utf-8"));
399
+ } catch {
400
+ return {};
401
+ }
402
+ }
403
+ function saveCache(rootDir, cache) {
404
+ const cachePath = join(rootDir, PATHS.CACHE);
405
+ const dir = dirname(cachePath);
406
+ if (!existsSync(dir)) {
407
+ mkdirSync(dir, { recursive: true });
408
+ }
409
+ writeFileSync(cachePath, JSON.stringify(cache, null, 2));
410
+ }
411
+ function computePackageHash(pkgDir) {
412
+ const hash = createHash("sha256");
413
+ const pkgJsonPath = join(pkgDir, "package.json");
414
+ if (existsSync(pkgJsonPath)) {
415
+ hash.update(readFileSync(pkgJsonPath));
416
+ }
417
+ const srcDir = join(pkgDir, "src");
418
+ if (existsSync(srcDir)) {
419
+ hashDirectory(srcDir, hash);
420
+ }
421
+ return hash.digest("hex");
422
+ }
423
+ function hashDirectory(dir, hash) {
424
+ const entries = readdirSync(dir).sort();
425
+ for (const entry of entries) {
426
+ const fullPath = join(dir, entry);
427
+ const stat = statSync(fullPath);
428
+ if (stat.isDirectory()) {
429
+ hashDirectory(fullPath, hash);
430
+ } else if (stat.isFile()) {
431
+ hash.update(fullPath);
432
+ hash.update(readFileSync(fullPath));
433
+ }
434
+ }
435
+ }
436
+ function updateCacheEntry(pkgDir, pkgName, cache) {
437
+ const hash = computePackageHash(pkgDir);
438
+ return {
439
+ ...cache,
440
+ [pkgName]: { hash, timestamp: (/* @__PURE__ */ new Date()).toISOString() }
441
+ };
442
+ }
443
+ function needsRebuild(pkgDir) {
444
+ const srcDir = join(pkgDir, "src");
445
+ const distDir = join(pkgDir, "dist");
446
+ if (!existsSync(distDir)) {
447
+ return true;
448
+ }
449
+ if (!existsSync(srcDir)) {
450
+ return false;
451
+ }
452
+ const srcMtime = getLatestMtime(srcDir);
453
+ const distMtime = getLatestMtime(distDir);
454
+ return srcMtime > distMtime;
455
+ }
456
+ function getLatestMtime(dir) {
457
+ let latest = 0;
458
+ try {
459
+ const entries = readdirSync(dir, { withFileTypes: true });
460
+ for (const entry of entries) {
461
+ const fullPath = join(dir, entry.name);
462
+ if (entry.isDirectory()) {
463
+ latest = Math.max(latest, getLatestMtime(fullPath));
464
+ } else {
465
+ latest = Math.max(latest, statSync(fullPath).mtimeMs);
466
+ }
467
+ }
468
+ } catch {
469
+ }
470
+ return latest;
471
+ }
472
+ function runBuildCheck(options) {
473
+ const { packages, noCache, onProgress } = options;
474
+ const result = { passed: [], failed: [], skipped: [], errors: {} };
475
+ const sorted = sortByBuildLayers(packages);
476
+ for (const pkg of sorted) {
477
+ if (!noCache && !needsRebuild(pkg.dir)) {
478
+ result.skipped.push(pkg.name);
479
+ onProgress?.(pkg.name, "skip");
480
+ continue;
481
+ }
482
+ const startMs = Date.now();
483
+ try {
484
+ execSync("pnpm run build", {
485
+ cwd: pkg.dir,
486
+ encoding: "utf-8",
487
+ timeout: 12e4,
488
+ stdio: ["pipe", "pipe", "pipe"]
489
+ });
490
+ const durationMs = Date.now() - startMs;
491
+ result.passed.push(pkg.name);
492
+ onProgress?.(pkg.name, "pass", durationMs);
493
+ } catch (err) {
494
+ const durationMs = Date.now() - startMs;
495
+ result.failed.push(pkg.name);
496
+ const rawErr = (err.stderr || err.stdout || err.message || "").trim();
497
+ result.errors[pkg.name] = rawErr.slice(0, 2e3) || `Build failed (exit code ${err.status ?? 1})`;
498
+ onProgress?.(pkg.name, "fail", durationMs);
499
+ }
500
+ }
501
+ return result;
502
+ }
503
+ function runLintCheck(options) {
504
+ const { packages, onProgress } = options;
505
+ const result = { passed: [], failed: [], skipped: [], errors: {} };
506
+ for (const pkg of packages) {
507
+ const srcDir = join(pkg.dir, "src");
508
+ if (!existsSync(srcDir)) {
509
+ result.skipped.push(pkg.name);
510
+ onProgress?.(pkg.name, "skip");
511
+ continue;
512
+ }
513
+ const startMs = Date.now();
514
+ try {
515
+ execSync("pnpm exec eslint .", {
516
+ cwd: pkg.dir,
517
+ encoding: "utf-8",
518
+ timeout: 6e4,
519
+ stdio: ["pipe", "pipe", "pipe"]
520
+ });
521
+ result.passed.push(pkg.name);
522
+ onProgress?.(pkg.name, "pass", Date.now() - startMs);
523
+ } catch (err) {
524
+ result.failed.push(pkg.name);
525
+ const rawErr = (err.stdout || err.stderr || err.message || "").trim();
526
+ result.errors[pkg.name] = rawErr.slice(0, 2e3) || `Lint failed (exit code ${err.status ?? 1})`;
527
+ onProgress?.(pkg.name, "fail", Date.now() - startMs);
528
+ }
529
+ }
530
+ return result;
531
+ }
532
+ function runTypeCheck(options) {
533
+ const { packages, onProgress } = options;
534
+ const result = { passed: [], failed: [], skipped: [], errors: {} };
535
+ for (const pkg of packages) {
536
+ const tsconfigPath = join(pkg.dir, "tsconfig.json");
537
+ if (!existsSync(tsconfigPath)) {
538
+ result.skipped.push(pkg.name);
539
+ onProgress?.(pkg.name, "skip");
540
+ continue;
541
+ }
542
+ const startMs = Date.now();
543
+ try {
544
+ execSync("pnpm exec tsc --noEmit", {
545
+ cwd: pkg.dir,
546
+ encoding: "utf-8",
547
+ timeout: 12e4,
548
+ stdio: ["pipe", "pipe", "pipe"]
549
+ });
550
+ result.passed.push(pkg.name);
551
+ onProgress?.(pkg.name, "pass", Date.now() - startMs);
552
+ } catch (err) {
553
+ result.failed.push(pkg.name);
554
+ const rawErr = (err.stdout || err.stderr || err.message || "").trim();
555
+ result.errors[pkg.name] = rawErr.slice(0, 2e3) || `Type check failed (exit code ${err.status ?? 1})`;
556
+ onProgress?.(pkg.name, "fail", Date.now() - startMs);
557
+ }
558
+ }
559
+ return result;
560
+ }
561
+ function runTestCheck(options) {
562
+ const { packages, onProgress } = options;
563
+ const result = { passed: [], failed: [], skipped: [], errors: {} };
564
+ for (const pkg of packages) {
565
+ let pkgJson;
566
+ try {
567
+ pkgJson = JSON.parse(
568
+ readFileSync(join(pkg.dir, "package.json"), "utf-8")
569
+ );
570
+ } catch {
571
+ result.skipped.push(pkg.name);
572
+ onProgress?.(pkg.name, "skip");
573
+ continue;
574
+ }
575
+ if (!pkgJson.scripts?.test) {
576
+ result.skipped.push(pkg.name);
577
+ onProgress?.(pkg.name, "skip");
578
+ continue;
579
+ }
580
+ const startMs = Date.now();
581
+ try {
582
+ execSync("pnpm run test", {
583
+ cwd: pkg.dir,
584
+ encoding: "utf-8",
585
+ timeout: 12e4,
586
+ stdio: ["pipe", "pipe", "pipe"]
587
+ });
588
+ result.passed.push(pkg.name);
589
+ onProgress?.(pkg.name, "pass", Date.now() - startMs);
590
+ } catch (err) {
591
+ result.failed.push(pkg.name);
592
+ const rawErr = (err.stdout || err.stderr || err.message || "").trim();
593
+ result.errors[pkg.name] = rawErr.slice(0, 2e3) || `Test failed (exit code ${err.status ?? 1})`;
594
+ onProgress?.(pkg.name, "fail", Date.now() - startMs);
595
+ }
596
+ }
597
+ return result;
598
+ }
599
+ function saveLastRun(rootDir, results, packages, submodules) {
600
+ const filePath = join(rootDir, PATHS.LAST_RUN);
601
+ const dir = dirname(filePath);
602
+ if (!existsSync(dir)) {
603
+ mkdirSync(dir, { recursive: true });
604
+ }
605
+ const data = {
606
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
607
+ results,
608
+ packages: packages.map((p) => ({
609
+ name: p.name,
610
+ dir: p.dir,
611
+ relativePath: p.relativePath,
612
+ repo: p.repo,
613
+ submodule: p.submodule
614
+ })),
615
+ submodules
616
+ };
617
+ writeFileSync(filePath, JSON.stringify(data, null, 2));
618
+ }
619
+ function loadLastRun(rootDir) {
620
+ const filePath = join(rootDir, PATHS.LAST_RUN);
621
+ if (!existsSync(filePath)) {
622
+ return null;
623
+ }
624
+ try {
625
+ return JSON.parse(readFileSync(filePath, "utf-8"));
626
+ } catch {
627
+ return null;
628
+ }
629
+ }
630
+
631
+ // src/runner/qa-orchestrator.ts
632
+ var SKIP_ALIASES = {
633
+ types: "typecheck",
634
+ "type-check": "typecheck",
635
+ tests: "test"
636
+ };
637
+ function runBuiltinChecks(options, packages, skipSet, results) {
638
+ const { rootDir, noCache } = options;
639
+ if (!skipSet.has("build")) {
640
+ results.build = runBuildCheck({
641
+ packages,
642
+ noCache,
643
+ onProgress: (pkg, status, durationMs) => options.onProgress?.("build", pkg, status, durationMs)
644
+ });
645
+ }
646
+ if (!skipSet.has("lint")) {
647
+ results.lint = runLintCheck({
648
+ packages,
649
+ onProgress: (pkg, status, durationMs) => options.onProgress?.("lint", pkg, status, durationMs)
650
+ });
651
+ }
652
+ if (!skipSet.has("typecheck")) {
653
+ results.typeCheck = runTypeCheck({
654
+ packages,
655
+ onProgress: (pkg, status, durationMs) => options.onProgress?.("typeCheck", pkg, status, durationMs)
656
+ });
657
+ }
658
+ if (!skipSet.has("test")) {
659
+ results.test = runTestCheck({
660
+ packages,
661
+ onProgress: (pkg, status, durationMs) => options.onProgress?.("test", pkg, status, durationMs)
662
+ });
663
+ }
664
+ }
665
+ async function runQA(options) {
666
+ const { rootDir, noCache } = options;
667
+ const skipSet = new Set(
668
+ (options.skipChecks ?? []).map((s) => SKIP_ALIASES[s.toLowerCase()] ?? s.toLowerCase())
669
+ );
670
+ const filter = { package: options.package, repo: options.repo, scope: options.scope };
671
+ const packages = getWorkspacePackages(rootDir, filter, options.packagesConfig);
672
+ let cache = noCache ? {} : loadCache(rootDir);
673
+ const results = {};
674
+ if (options.checks && options.checks.length > 0) {
675
+ const activeChecks = skipSet.size > 0 ? options.checks.filter((c) => !skipSet.has(c.id.toLowerCase())) : options.checks;
676
+ Object.assign(results, runCustomChecks(
677
+ activeChecks,
678
+ packages,
679
+ rootDir,
680
+ (checkId, pkg, status, durationMs) => {
681
+ options.onProgress?.(checkId, pkg, status, durationMs);
682
+ }
683
+ ));
684
+ } else {
685
+ runBuiltinChecks(options, packages, skipSet, results);
686
+ }
687
+ if (!noCache) {
688
+ for (const pkg of packages) {
689
+ cache = updateCacheEntry(pkg.dir, pkg.name, cache);
690
+ }
691
+ saveCache(rootDir, cache);
692
+ }
693
+ const submodules = {};
694
+ for (const pkg of packages) {
695
+ if (pkg.submodule && !submodules[pkg.repo]) {
696
+ submodules[pkg.repo] = pkg.submodule;
697
+ }
698
+ }
699
+ saveLastRun(rootDir, results, packages, Object.keys(submodules).length > 0 ? submodules : void 0);
700
+ return { results, packages };
701
+ }
702
+ function loadBaseline(rootDir) {
703
+ const path = join(rootDir, PATHS.BASELINE);
704
+ if (!existsSync(path)) {
705
+ return null;
706
+ }
707
+ try {
708
+ return JSON.parse(readFileSync(path, "utf-8"));
709
+ } catch {
710
+ return null;
711
+ }
712
+ }
713
+ function saveBaseline(rootDir, snapshot) {
714
+ const path = join(rootDir, PATHS.BASELINE);
715
+ const dir = dirname(path);
716
+ if (!existsSync(dir)) {
717
+ mkdirSync(dir, { recursive: true });
718
+ }
719
+ writeFileSync(path, JSON.stringify(snapshot, null, 2));
720
+ }
721
+ function getGitInfo(rootDir) {
722
+ try {
723
+ const commit = execSync("git rev-parse --short HEAD", {
724
+ cwd: rootDir,
725
+ encoding: "utf-8"
726
+ }).trim();
727
+ const branch = execSync("git rev-parse --abbrev-ref HEAD", {
728
+ cwd: rootDir,
729
+ encoding: "utf-8"
730
+ }).trim();
731
+ return { commit, branch };
732
+ } catch {
733
+ return { commit: "unknown", branch: "unknown" };
734
+ }
735
+ }
736
+ function createBaselineFromResults(results, rootDir) {
737
+ const git = getGitInfo(rootDir);
738
+ const snapshot = {
739
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
740
+ git,
741
+ results: {}
742
+ };
743
+ for (const ct of Object.keys(results)) {
744
+ const r = results[ct];
745
+ snapshot.results[ct] = {
746
+ passed: r.passed.length,
747
+ failed: r.failed.length,
748
+ failedPackages: [...r.failed]
749
+ };
750
+ }
751
+ return snapshot;
752
+ }
753
+ async function captureBaseline(rootDir) {
754
+ const { results } = await runQA({ rootDir });
755
+ const snapshot = createBaselineFromResults(results, rootDir);
756
+ saveBaseline(rootDir, snapshot);
757
+ return snapshot;
758
+ }
759
+
760
+ // src/baseline/baseline-comparator.ts
761
+ function compareWithBaseline(results, baseline) {
762
+ const diff = {};
763
+ const checkTypes = [.../* @__PURE__ */ new Set([...Object.keys(results), ...Object.keys(baseline.results)])];
764
+ for (const ct of checkTypes) {
765
+ const current = new Set(results[ct]?.failed ?? []);
766
+ const baselineFailed = new Set(baseline.results[ct]?.failedPackages ?? []);
767
+ const newFailures = [...current].filter((p) => !baselineFailed.has(p));
768
+ const fixed = [...baselineFailed].filter((p) => !current.has(p));
769
+ const stillFailing = [...current].filter((p) => baselineFailed.has(p));
770
+ diff[ct] = {
771
+ newFailures,
772
+ fixed,
773
+ stillFailing,
774
+ delta: current.size - baselineFailed.size
775
+ };
776
+ }
777
+ return diff;
778
+ }
779
+ function loadHistory(rootDir) {
780
+ const path = join(rootDir, PATHS.HISTORY);
781
+ if (!existsSync(path)) {
782
+ return [];
783
+ }
784
+ try {
785
+ return JSON.parse(readFileSync(path, "utf-8"));
786
+ } catch {
787
+ return [];
788
+ }
789
+ }
790
+ function saveHistory(rootDir, entries) {
791
+ const path = join(rootDir, PATHS.HISTORY);
792
+ const dir = dirname(path);
793
+ if (!existsSync(dir)) {
794
+ mkdirSync(dir, { recursive: true });
795
+ }
796
+ writeFileSync(path, JSON.stringify(entries, null, 2));
797
+ }
798
+ function createHistoryEntry(results, rootDir, packages) {
799
+ let commit = "unknown";
800
+ let branch = "unknown";
801
+ let message = "";
802
+ try {
803
+ commit = execSync("git rev-parse --short HEAD", { cwd: rootDir, encoding: "utf-8" }).trim();
804
+ branch = execSync("git rev-parse --abbrev-ref HEAD", { cwd: rootDir, encoding: "utf-8" }).trim();
805
+ message = execSync("git log -1 --format=%s", { cwd: rootDir, encoding: "utf-8" }).trim();
806
+ } catch {
807
+ }
808
+ const hasFailures = Object.values(results).some((r) => r.failed.length > 0);
809
+ const summary = {};
810
+ const failedPackages = {};
811
+ for (const ct of Object.keys(results)) {
812
+ const r = results[ct];
813
+ summary[ct] = {
814
+ passed: r.passed.length,
815
+ failed: r.failed.length,
816
+ skipped: r.skipped.length
817
+ };
818
+ failedPackages[ct] = [...r.failed];
819
+ }
820
+ let submodules;
821
+ if (packages) {
822
+ const subs = {};
823
+ for (const pkg of packages) {
824
+ if (pkg.submodule && !subs[pkg.repo]) {
825
+ subs[pkg.repo] = pkg.submodule;
826
+ }
827
+ }
828
+ if (Object.keys(subs).length > 0) {
829
+ submodules = subs;
830
+ }
831
+ }
832
+ return {
833
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
834
+ git: { commit, branch, message },
835
+ submodules,
836
+ status: hasFailures ? "failed" : "passed",
837
+ summary,
838
+ failedPackages
839
+ };
840
+ }
841
+ function appendEntry(rootDir, entry) {
842
+ const history = loadHistory(rootDir);
843
+ history.push(entry);
844
+ while (history.length > HISTORY_MAX_ENTRIES) {
845
+ history.shift();
846
+ }
847
+ saveHistory(rootDir, history);
848
+ }
849
+ function collectCheckTypes(entries) {
850
+ const s = /* @__PURE__ */ new Set();
851
+ for (const e of entries) {
852
+ for (const k of Object.keys(e.summary)) {
853
+ s.add(k);
854
+ }
855
+ }
856
+ return [...s];
857
+ }
858
+ function analyzeTrends(history, window = TRENDS_WINDOW) {
859
+ if (history.length < 2) {
860
+ return [];
861
+ }
862
+ const windowEntries = history.slice(-window);
863
+ const first = windowEntries[0];
864
+ const last = windowEntries[windowEntries.length - 1];
865
+ const results = [];
866
+ for (const ct of collectCheckTypes([first, last])) {
867
+ const previous = first.summary[ct]?.failed ?? 0;
868
+ const current = last.summary[ct]?.failed ?? 0;
869
+ const delta = current - previous;
870
+ let trend;
871
+ if (delta > 0) {
872
+ trend = "regression";
873
+ } else if (delta < 0) {
874
+ trend = "improvement";
875
+ } else {
876
+ trend = "no-change";
877
+ }
878
+ results.push({ checkType: ct, label: getCheckLabel(ct), icon: getCheckIcon(ct), previous, current, delta, trend });
879
+ }
880
+ return results;
881
+ }
882
+ function analyzeEnrichedTrends(history, window = TRENDS_WINDOW) {
883
+ if (history.length < 2) {
884
+ return [];
885
+ }
886
+ const windowEntries = history.slice(-window);
887
+ const first = windowEntries[0];
888
+ const last = windowEntries[windowEntries.length - 1];
889
+ const results = [];
890
+ for (const ct of collectCheckTypes(windowEntries)) {
891
+ const timeSeries = windowEntries.map((entry) => ({
892
+ timestamp: entry.timestamp,
893
+ gitCommit: entry.git.commit,
894
+ gitBranch: entry.git.branch,
895
+ gitMessage: entry.git.message,
896
+ passed: entry.summary[ct]?.passed ?? 0,
897
+ failed: entry.summary[ct]?.failed ?? 0,
898
+ skipped: entry.summary[ct]?.skipped ?? 0
899
+ }));
900
+ const changelog = [];
901
+ const deltas = [];
902
+ for (let i = 1; i < windowEntries.length; i++) {
903
+ const prev = windowEntries[i - 1];
904
+ const curr = windowEntries[i];
905
+ const prevFailed = new Set(prev.failedPackages[ct] ?? []);
906
+ const currFailed = curr.failedPackages[ct] ?? [];
907
+ const currFailedSet = new Set(currFailed);
908
+ const newFailures = currFailed.filter((p) => !prevFailed.has(p));
909
+ const fixed = [...prevFailed].filter((p) => !currFailedSet.has(p));
910
+ const delta2 = currFailed.length - prevFailed.size;
911
+ deltas.push(delta2);
912
+ if (newFailures.length > 0 || fixed.length > 0) {
913
+ changelog.push({
914
+ timestamp: curr.timestamp,
915
+ gitCommit: curr.git.commit,
916
+ gitMessage: curr.git.message,
917
+ newFailures,
918
+ fixed,
919
+ delta: delta2
920
+ });
921
+ }
922
+ }
923
+ const previous = first.summary[ct]?.failed ?? 0;
924
+ const current = last.summary[ct]?.failed ?? 0;
925
+ const delta = current - previous;
926
+ let trend;
927
+ if (delta > 0) {
928
+ trend = "regression";
929
+ } else if (delta < 0) {
930
+ trend = "improvement";
931
+ } else {
932
+ trend = "no-change";
933
+ }
934
+ const velocity = deltas.length > 0 ? deltas.reduce((sum, d) => sum + d, 0) / deltas.length : 0;
935
+ results.push({
936
+ checkType: ct,
937
+ label: getCheckLabel(ct),
938
+ icon: getCheckIcon(ct),
939
+ timeSeries,
940
+ changelog,
941
+ current,
942
+ previous,
943
+ delta,
944
+ trend,
945
+ velocity: Math.round(velocity * 100) / 100
946
+ });
947
+ }
948
+ return results;
949
+ }
950
+
951
+ // src/history/regression-detector.ts
952
+ function detectRegressions(history) {
953
+ if (history.length < 2) {
954
+ return { hasRegressions: false, regressions: [] };
955
+ }
956
+ const previous = history[history.length - 2];
957
+ const current = history[history.length - 1];
958
+ const regressions = [];
959
+ const checkTypes = [.../* @__PURE__ */ new Set([...Object.keys(previous.failedPackages), ...Object.keys(current.failedPackages)])];
960
+ for (const ct of checkTypes) {
961
+ const prevFailed = new Set(previous.failedPackages[ct] ?? []);
962
+ const currFailed = current.failedPackages[ct] ?? [];
963
+ const newFailures = currFailed.filter((p) => !prevFailed.has(p));
964
+ const delta = currFailed.length - prevFailed.size;
965
+ if (newFailures.length > 0) {
966
+ regressions.push({
967
+ checkType: ct,
968
+ delta,
969
+ newFailures
970
+ });
971
+ }
972
+ }
973
+ return {
974
+ hasRegressions: regressions.length > 0,
975
+ regressions
976
+ };
977
+ }
978
+
979
+ // src/history/package-timeline.ts
980
+ function buildEntries(history, packageName) {
981
+ let repo = "unknown";
982
+ const entries = [];
983
+ for (let i = history.length - 1; i >= 0; i--) {
984
+ const h = history[i];
985
+ const checks = {};
986
+ let found = false;
987
+ for (const ct of Object.keys(h.summary)) {
988
+ const failedList = h.failedPackages[ct] ?? [];
989
+ const summaryEntry = h.summary[ct];
990
+ if (failedList.includes(packageName)) {
991
+ checks[ct] = "failed";
992
+ found = true;
993
+ } else if (summaryEntry && (summaryEntry.passed > 0 || summaryEntry.failed > 0)) {
994
+ checks[ct] = "passed";
995
+ found = true;
996
+ } else {
997
+ checks[ct] = "skipped";
998
+ }
999
+ }
1000
+ if (!found) {
1001
+ continue;
1002
+ }
1003
+ let submoduleCommit;
1004
+ if (h.submodules) {
1005
+ for (const [repoName, info] of Object.entries(h.submodules)) {
1006
+ if (repoName === repo || repo === "unknown") {
1007
+ submoduleCommit = info.commit;
1008
+ if (repo === "unknown") {
1009
+ repo = repoName;
1010
+ }
1011
+ }
1012
+ }
1013
+ }
1014
+ entries.push({
1015
+ timestamp: h.timestamp,
1016
+ git: h.git,
1017
+ submoduleCommit,
1018
+ checks
1019
+ });
1020
+ }
1021
+ return { entries, repo };
1022
+ }
1023
+ function computeFlakyScore(entries) {
1024
+ const allCheckTypes = /* @__PURE__ */ new Set();
1025
+ for (const entry of entries) {
1026
+ for (const k of Object.keys(entry.checks)) {
1027
+ allCheckTypes.add(k);
1028
+ }
1029
+ }
1030
+ const flakyChecks = [];
1031
+ let totalFlips = 0;
1032
+ let totalTransitions = 0;
1033
+ for (const ct of allCheckTypes) {
1034
+ let flips = 0;
1035
+ let transitions = 0;
1036
+ for (let i = 1; i < entries.length; i++) {
1037
+ const prev = entries[i - 1].checks[ct];
1038
+ const curr = entries[i].checks[ct];
1039
+ if (prev === "skipped" || curr === "skipped" || !prev || !curr) {
1040
+ continue;
1041
+ }
1042
+ transitions++;
1043
+ if (prev !== curr) {
1044
+ flips++;
1045
+ }
1046
+ }
1047
+ if (transitions > 0 && flips / transitions > 0.3) {
1048
+ flakyChecks.push(ct);
1049
+ }
1050
+ totalFlips += flips;
1051
+ totalTransitions += transitions;
1052
+ }
1053
+ return {
1054
+ flakyScore: totalTransitions > 0 ? Math.min(1, totalFlips / totalTransitions) : 0,
1055
+ flakyChecks
1056
+ };
1057
+ }
1058
+ function computeStreak(entries) {
1059
+ const latest = entries[0];
1060
+ if (!latest) {
1061
+ return { status: "passing", count: 0 };
1062
+ }
1063
+ const streakStatus = Object.values(latest.checks).some((v) => v === "failed") ? "failing" : "passing";
1064
+ let count = 1;
1065
+ for (let i = 1; i < entries.length; i++) {
1066
+ const eFail = Object.values(entries[i].checks).some((v) => v === "failed");
1067
+ if ((eFail ? "failing" : "passing") !== streakStatus) {
1068
+ break;
1069
+ }
1070
+ count++;
1071
+ }
1072
+ return { status: streakStatus, count };
1073
+ }
1074
+ function getPackageTimeline(history, packageName) {
1075
+ const { entries, repo } = buildEntries(history, packageName);
1076
+ const { flakyScore, flakyChecks } = computeFlakyScore(entries);
1077
+ let firstFailure;
1078
+ for (let i = entries.length - 1; i >= 0; i--) {
1079
+ if (Object.values(entries[i].checks).some((v) => v === "failed")) {
1080
+ firstFailure = entries[i].timestamp;
1081
+ }
1082
+ }
1083
+ return {
1084
+ packageName,
1085
+ repo,
1086
+ entries,
1087
+ flakyScore: Math.round(flakyScore * 100) / 100,
1088
+ flakyChecks,
1089
+ firstFailure,
1090
+ currentStreak: computeStreak(entries)
1091
+ };
1092
+ }
1093
+
1094
+ // src/report/json-reporter.ts
1095
+ function buildJsonReport(results, diff) {
1096
+ const hasFailures = Object.values(results).some((r) => r.failed.length > 0);
1097
+ const summary = {};
1098
+ const failures = {};
1099
+ const errors = {};
1100
+ for (const ct of Object.keys(results)) {
1101
+ const r = results[ct];
1102
+ const total = r.passed.length + r.failed.length + r.skipped.length;
1103
+ summary[ct] = {
1104
+ total,
1105
+ passed: r.passed.length,
1106
+ failed: r.failed.length,
1107
+ skipped: r.skipped.length
1108
+ };
1109
+ failures[ct] = [...r.failed];
1110
+ errors[ct] = { ...r.errors };
1111
+ }
1112
+ return {
1113
+ status: hasFailures ? "failed" : "passed",
1114
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1115
+ summary,
1116
+ failures,
1117
+ errors,
1118
+ baseline: diff ?? null
1119
+ };
1120
+ }
1121
+ function buildDetailedJsonReport(results, grouped, diff) {
1122
+ const base = buildJsonReport(results, diff);
1123
+ return { ...base, grouped };
1124
+ }
1125
+ function icon(ct) {
1126
+ return getCheckIcon(ct);
1127
+ }
1128
+ function label(ct) {
1129
+ return getCheckLabel(ct);
1130
+ }
1131
+ function buildBaselineDiffLines(diff) {
1132
+ const lines = [];
1133
+ for (const ct of Object.keys(diff)) {
1134
+ const d = diff[ct];
1135
+ if (d.newFailures.length > 0) {
1136
+ lines.push(`${icon(ct)} ${label(ct)}: +${d.newFailures.length} new failures`);
1137
+ for (const pkg of d.newFailures) {
1138
+ lines.push(` - ${pkg}`);
1139
+ }
1140
+ }
1141
+ if (d.fixed.length > 0) {
1142
+ lines.push(`${icon(ct)} ${label(ct)}: -${d.fixed.length} fixed`);
1143
+ }
1144
+ }
1145
+ return lines;
1146
+ }
1147
+ function buildRunReport(results, diff) {
1148
+ const sections = [];
1149
+ const summaryLines = [];
1150
+ let totalPassed = 0;
1151
+ let totalFailed = 0;
1152
+ let totalSkipped = 0;
1153
+ for (const ct of Object.keys(results)) {
1154
+ const r = results[ct];
1155
+ const total = r.passed.length + r.failed.length + r.skipped.length;
1156
+ const pct = total > 0 ? Math.round(r.passed.length / total * 100) : 100;
1157
+ const status = r.failed.length === 0 ? "PASS" : "FAIL";
1158
+ summaryLines.push(`${status} ${icon(ct)} ${label(ct).padEnd(12)} ${r.passed.length}/${total} passed (${pct}%)`);
1159
+ if (r.failed.length > 0) {
1160
+ for (const pkg of r.failed.slice(0, 5)) {
1161
+ summaryLines.push(` - ${pkg}`);
1162
+ }
1163
+ if (r.failed.length > 5) {
1164
+ summaryLines.push(` ... and ${r.failed.length - 5} more`);
1165
+ }
1166
+ }
1167
+ totalPassed += r.passed.length;
1168
+ totalFailed += r.failed.length;
1169
+ totalSkipped += r.skipped.length;
1170
+ }
1171
+ sections.push({ header: "QA Summary Report", lines: summaryLines });
1172
+ if (diff) {
1173
+ const diffLines = buildBaselineDiffLines(diff);
1174
+ if (diffLines.length > 0) {
1175
+ sections.push({ header: "Baseline Comparison", lines: diffLines });
1176
+ }
1177
+ }
1178
+ sections.push({
1179
+ header: "Totals",
1180
+ lines: [`Total: ${totalPassed} passed, ${totalFailed} failed, ${totalSkipped} skipped`]
1181
+ });
1182
+ return sections;
1183
+ }
1184
+ function buildHistoryTable(history, limit = 20) {
1185
+ const entries = history.slice(-limit);
1186
+ const lines = [];
1187
+ for (const entry of entries) {
1188
+ const date = new Date(entry.timestamp).toLocaleDateString();
1189
+ const status = entry.status === "passed" ? "PASS" : "FAIL";
1190
+ const summary = Object.keys(entry.summary).map((ct) => {
1191
+ const s = entry.summary[ct];
1192
+ return `${icon(ct)} ${s.failed}F`;
1193
+ }).join(" ");
1194
+ lines.push(`${date} ${entry.git.commit} ${status} ${summary} ${entry.git.message.slice(0, 40)}`);
1195
+ }
1196
+ return [{ header: `QA History (last ${entries.length})`, lines }];
1197
+ }
1198
+ function buildTrendsReport(trends, history) {
1199
+ if (trends.length === 0) {
1200
+ return [{ header: "QA Trends", lines: ["Not enough history (need at least 2 entries)"] }];
1201
+ }
1202
+ const lines = [];
1203
+ for (const t of trends) {
1204
+ const arrow = t.delta > 0 ? `+${t.delta} (regression)` : t.delta < 0 ? `${t.delta} (improvement)` : "\u2192 no change";
1205
+ lines.push(`${icon(t.checkType)} ${label(t.checkType).padEnd(12)} ${t.previous} \u2192 ${t.current} ${arrow}`);
1206
+ }
1207
+ if (history.length >= 2) {
1208
+ const first = history[Math.max(0, history.length - 10)];
1209
+ const last = history[history.length - 1];
1210
+ lines.push("");
1211
+ lines.push(`Period: ${new Date(first.timestamp).toLocaleDateString()} \u2192 ${new Date(last.timestamp).toLocaleDateString()}`);
1212
+ }
1213
+ return [{ header: "QA Trends", lines }];
1214
+ }
1215
+ function buildRegressionsReport(result, history) {
1216
+ if (history.length < 2) {
1217
+ return [{ header: "Regression Detection", lines: ["Not enough history (need at least 2 entries)"] }];
1218
+ }
1219
+ const prev = history[history.length - 2];
1220
+ const curr = history[history.length - 1];
1221
+ const lines = [
1222
+ `Comparing: ${prev.git.commit} \u2192 ${curr.git.commit}`,
1223
+ ""
1224
+ ];
1225
+ if (!result.hasRegressions) {
1226
+ lines.push("No regressions detected.");
1227
+ return [{ header: "Regression Detection", lines }];
1228
+ }
1229
+ for (const r of result.regressions) {
1230
+ lines.push(`${r.checkType}: +${r.newFailures.length} new failures`);
1231
+ for (const pkg of r.newFailures) {
1232
+ lines.push(` - ${pkg}`);
1233
+ }
1234
+ }
1235
+ lines.push("");
1236
+ lines.push("REGRESSIONS DETECTED!");
1237
+ return [{ header: "Regression Detection", lines }];
1238
+ }
1239
+ function buildBaselineReport(baseline) {
1240
+ if (!baseline) {
1241
+ return [{ header: "Baseline Status", lines: ["No baseline captured yet. Run baseline:update first."] }];
1242
+ }
1243
+ const lines = [
1244
+ `Captured: ${new Date(baseline.timestamp).toLocaleString()}`,
1245
+ `Git: ${baseline.git.commit} (${baseline.git.branch})`,
1246
+ ""
1247
+ ];
1248
+ for (const ct of Object.keys(baseline.results)) {
1249
+ const r = baseline.results[ct];
1250
+ lines.push(`${icon(ct)} ${label(ct).padEnd(12)} ${r.passed} passed, ${r.failed} failed`);
1251
+ if (r.failedPackages.length > 0) {
1252
+ const shown = r.failedPackages.slice(0, 3);
1253
+ for (const pkg of shown) {
1254
+ lines.push(` - ${pkg}`);
1255
+ }
1256
+ if (r.failedPackages.length > 3) {
1257
+ lines.push(` ... and ${r.failedPackages.length - 3} more`);
1258
+ }
1259
+ }
1260
+ }
1261
+ return [{ header: "Baseline Status", lines }];
1262
+ }
1263
+ function checkTag(status, ct) {
1264
+ const short = ct === "typeCheck" ? "types" : ct;
1265
+ if (status === "failed") {
1266
+ return short.toUpperCase();
1267
+ }
1268
+ if (status === "skipped") {
1269
+ return `-${short}-`;
1270
+ }
1271
+ return short;
1272
+ }
1273
+ function getErrorPreview(raw) {
1274
+ const errLines = raw.split("\n").filter((l) => l.trim().length > 0);
1275
+ for (const el of errLines) {
1276
+ const cleaned = el.replace(/^Command failed: .*/, "").trim();
1277
+ if (cleaned.length > 0) {
1278
+ return cleaned.replace(/\/[^\s]*\/kb-labs\//g, "").slice(0, 100);
1279
+ }
1280
+ }
1281
+ return "";
1282
+ }
1283
+ function renderPackageLines(pkg, lines) {
1284
+ const hasFail = Object.values(pkg.checks).some((v) => v === "failed");
1285
+ const status = hasFail ? "FAIL" : "PASS";
1286
+ const tags = Object.keys(pkg.checks).map((ct) => checkTag(pkg.checks[ct], ct)).join(" ");
1287
+ lines.push(` ${status} ${pkg.name.padEnd(40)} ${tags}`);
1288
+ if (hasFail) {
1289
+ for (const ct of Object.keys(pkg.checks)) {
1290
+ if (pkg.checks[ct] === "failed") {
1291
+ const preview = getErrorPreview((pkg.errors[ct] ?? "").trim());
1292
+ lines.push(` ${ct}: ${preview || "failed"}`);
1293
+ }
1294
+ }
1295
+ }
1296
+ }
1297
+ function renderCategoryLines(catKey, grouped) {
1298
+ const cat = grouped.categories[catKey];
1299
+ const lines = [`PASS ${cat.summary.passed} | FAIL ${cat.summary.failed}`, ""];
1300
+ for (const repoKey of Object.keys(cat.repos).sort()) {
1301
+ const repo = cat.repos[repoKey];
1302
+ lines.push(` ${repoKey} (${repo.summary.total} packages)`);
1303
+ const sorted = [...repo.packages].sort((a, b) => {
1304
+ const aFail = Object.values(a.checks).some((v) => v === "failed") ? 0 : 1;
1305
+ const bFail = Object.values(b.checks).some((v) => v === "failed") ? 0 : 1;
1306
+ if (aFail !== bFail) {
1307
+ return aFail - bFail;
1308
+ }
1309
+ return a.name.localeCompare(b.name);
1310
+ });
1311
+ for (const pkg of sorted) {
1312
+ renderPackageLines(pkg, lines);
1313
+ }
1314
+ lines.push("");
1315
+ }
1316
+ return lines;
1317
+ }
1318
+ function buildDetailedRunReport(grouped, diff) {
1319
+ const sections = [];
1320
+ const categoryKeys = Object.keys(grouped.categories).sort((a, b) => {
1321
+ if (a === "uncategorized") {
1322
+ return 1;
1323
+ }
1324
+ if (b === "uncategorized") {
1325
+ return -1;
1326
+ }
1327
+ return a.localeCompare(b);
1328
+ });
1329
+ for (const catKey of categoryKeys) {
1330
+ const cat = grouped.categories[catKey];
1331
+ sections.push({ header: `${cat.label} (${cat.summary.total} packages)`, lines: renderCategoryLines(catKey, grouped) });
1332
+ }
1333
+ if (diff) {
1334
+ const diffLines = buildBaselineDiffLines(diff);
1335
+ if (diffLines.length > 0) {
1336
+ sections.push({ header: "Baseline Comparison", lines: diffLines });
1337
+ }
1338
+ }
1339
+ let totalPassed = 0;
1340
+ let totalFailed = 0;
1341
+ for (const catKey of categoryKeys) {
1342
+ totalPassed += grouped.categories[catKey].summary.passed;
1343
+ totalFailed += grouped.categories[catKey].summary.failed;
1344
+ }
1345
+ sections.push({
1346
+ header: "Totals",
1347
+ lines: [`Total: ${totalPassed} passed, ${totalFailed} failed (${categoryKeys.length} categories)`]
1348
+ });
1349
+ return sections;
1350
+ }
1351
+
1352
+ // src/report/grouped-reporter.ts
1353
+ function emptyGroupSummary(checkTypes) {
1354
+ const checks = {};
1355
+ for (const ct of checkTypes) {
1356
+ checks[ct] = { passed: 0, failed: 0, skipped: 0 };
1357
+ }
1358
+ return { total: 0, passed: 0, failed: 0, checks };
1359
+ }
1360
+ function resolveCheckStatus(pkgName, ct, results) {
1361
+ const r = results[ct];
1362
+ if (!r) {
1363
+ return "skipped";
1364
+ }
1365
+ if (r.failed.includes(pkgName)) {
1366
+ return "failed";
1367
+ }
1368
+ if (r.passed.includes(pkgName)) {
1369
+ return "passed";
1370
+ }
1371
+ return "skipped";
1372
+ }
1373
+ function buildPackageStatus(pkg, results, category) {
1374
+ const checks = {};
1375
+ const errors = {};
1376
+ for (const ct of Object.keys(results)) {
1377
+ checks[ct] = resolveCheckStatus(pkg.name, ct, results);
1378
+ if (checks[ct] === "failed" && results[ct]?.errors[pkg.name]) {
1379
+ errors[ct] = results[ct].errors[pkg.name];
1380
+ }
1381
+ }
1382
+ return {
1383
+ name: pkg.name,
1384
+ repo: pkg.repo,
1385
+ category,
1386
+ checks,
1387
+ errors
1388
+ };
1389
+ }
1390
+ function addToSummary(summary, status) {
1391
+ summary.total++;
1392
+ const hasFail = Object.values(status.checks).some((v) => v === "failed");
1393
+ if (hasFail) {
1394
+ summary.failed++;
1395
+ } else {
1396
+ summary.passed++;
1397
+ }
1398
+ for (const ct of Object.keys(status.checks)) {
1399
+ const s = status.checks[ct];
1400
+ if (!summary.checks[ct]) {
1401
+ summary.checks[ct] = { passed: 0, failed: 0, skipped: 0 };
1402
+ }
1403
+ if (s === "passed") {
1404
+ summary.checks[ct].passed++;
1405
+ } else if (s === "failed") {
1406
+ summary.checks[ct].failed++;
1407
+ } else {
1408
+ summary.checks[ct].skipped++;
1409
+ }
1410
+ }
1411
+ }
1412
+ function groupResults(results, packages, categoryMap, config) {
1413
+ const checkTypes = Object.keys(results);
1414
+ const grouped = { categories: {} };
1415
+ for (const pkg of packages) {
1416
+ const categoryKeys = categoryMap.get(pkg.name) ?? ["uncategorized"];
1417
+ for (const categoryKey of categoryKeys) {
1418
+ const status = buildPackageStatus(pkg, results, categoryKey);
1419
+ if (!grouped.categories[categoryKey]) {
1420
+ const label2 = categoryKey === "uncategorized" ? "Uncategorized" : config?.categories?.[categoryKey]?.label ?? categoryKey;
1421
+ grouped.categories[categoryKey] = {
1422
+ label: label2,
1423
+ repos: {},
1424
+ summary: emptyGroupSummary(checkTypes)
1425
+ };
1426
+ }
1427
+ const categoryGroup = grouped.categories[categoryKey];
1428
+ if (!categoryGroup.repos[pkg.repo]) {
1429
+ categoryGroup.repos[pkg.repo] = {
1430
+ packages: [],
1431
+ summary: emptyGroupSummary(checkTypes)
1432
+ };
1433
+ }
1434
+ const repoGroup = categoryGroup.repos[pkg.repo];
1435
+ repoGroup.packages.push(status);
1436
+ addToSummary(repoGroup.summary, status);
1437
+ addToSummary(categoryGroup.summary, status);
1438
+ }
1439
+ }
1440
+ return grouped;
1441
+ }
1442
+
1443
+ // src/report/error-grouping.ts
1444
+ function extractPattern(errorText, checkType) {
1445
+ if (checkType === "lint") {
1446
+ const ruleMatch = errorText.match(/(\S+\/[\w-]+|no-[\w-]+)/);
1447
+ if (ruleMatch?.[1]) {
1448
+ return ruleMatch[1];
1449
+ }
1450
+ }
1451
+ if (checkType === "typeCheck") {
1452
+ const tsMatch = errorText.match(/TS(\d{4,5})/);
1453
+ if (tsMatch) {
1454
+ return `TS${tsMatch[1]}`;
1455
+ }
1456
+ }
1457
+ if (checkType === "test") {
1458
+ const failMatch = errorText.match(/FAIL\s+(\S+)/);
1459
+ if (failMatch) {
1460
+ return `FAIL: ${failMatch[1]}`;
1461
+ }
1462
+ }
1463
+ if (checkType === "build") {
1464
+ if (errorText.includes("Cannot find module")) {
1465
+ return "Cannot find module";
1466
+ }
1467
+ if (errorText.includes("Module not found")) {
1468
+ return "Module not found";
1469
+ }
1470
+ }
1471
+ const firstLine = errorText.split("\n").find((l) => l.trim().length > 0)?.trim() ?? "";
1472
+ return firstLine.slice(0, 100) || "Unknown error";
1473
+ }
1474
+ function groupErrors(results) {
1475
+ const groupMap = /* @__PURE__ */ new Map();
1476
+ let ungrouped = 0;
1477
+ for (const ct of Object.keys(results)) {
1478
+ const check = results[ct];
1479
+ if (!check.errors) {
1480
+ continue;
1481
+ }
1482
+ for (const [pkgName, errorText] of Object.entries(check.errors)) {
1483
+ const pattern = extractPattern(errorText, ct);
1484
+ const key = `${ct}::${pattern}`;
1485
+ const existing = groupMap.get(key);
1486
+ if (existing) {
1487
+ existing.count++;
1488
+ existing.packages.push(pkgName);
1489
+ } else {
1490
+ groupMap.set(key, {
1491
+ pattern,
1492
+ count: 1,
1493
+ packages: [pkgName],
1494
+ checkType: ct,
1495
+ example: errorText.slice(0, 200)
1496
+ });
1497
+ }
1498
+ }
1499
+ }
1500
+ const groups = [];
1501
+ for (const group of groupMap.values()) {
1502
+ if (group.count === 1) {
1503
+ ungrouped++;
1504
+ } else {
1505
+ groups.push(group);
1506
+ }
1507
+ }
1508
+ groups.sort((a, b) => b.count - a.count);
1509
+ return { groups, ungrouped };
1510
+ }
1511
+
1512
+ // src/categories/category-resolver.ts
1513
+ function matchesPattern2(packageName, repo, pattern) {
1514
+ if (pattern.includes("/") && pattern.endsWith("/*")) {
1515
+ const repoPrefix = pattern.slice(0, -2);
1516
+ return repo === repoPrefix;
1517
+ }
1518
+ if (!pattern.includes("*")) {
1519
+ return packageName === pattern;
1520
+ }
1521
+ const prefix = pattern.slice(0, pattern.indexOf("*"));
1522
+ return packageName.startsWith(prefix);
1523
+ }
1524
+ function resolveCategories(packages, config) {
1525
+ const map = /* @__PURE__ */ new Map();
1526
+ if (!config?.categories) {
1527
+ for (const pkg of packages) {
1528
+ map.set(pkg.name, ["uncategorized"]);
1529
+ }
1530
+ return map;
1531
+ }
1532
+ const categoryEntries = Object.entries(config.categories);
1533
+ for (const pkg of packages) {
1534
+ const matched = [];
1535
+ for (const [categoryKey, categoryConfig] of categoryEntries) {
1536
+ for (const pattern of categoryConfig.packages) {
1537
+ if (matchesPattern2(pkg.name, pkg.repo, pattern)) {
1538
+ matched.push(categoryKey);
1539
+ break;
1540
+ }
1541
+ }
1542
+ }
1543
+ map.set(pkg.name, matched.length > 0 ? matched : ["uncategorized"]);
1544
+ }
1545
+ return map;
1546
+ }
1547
+
1548
+ export { analyzeEnrichedTrends, analyzeTrends, appendEntry, buildBaselineReport, buildDetailedJsonReport, buildDetailedRunReport, buildHistoryTable, buildJsonReport, buildRegressionsReport, buildRunReport, buildTrendsReport, captureBaseline, collectSubmoduleInfo, compareWithBaseline, createBaselineFromResults, createHistoryEntry, detectRegressions, getPackageTimeline, getSubmoduleInfo, getWorkspacePackages, groupErrors, groupResults, loadBaseline, loadHistory, loadLastRun, resolveCategories, runLintCheck, runQA, runTestCheck, runTypeCheck, saveBaseline, saveHistory, saveLastRun };
1549
+ //# sourceMappingURL=index.js.map
1550
+ //# sourceMappingURL=index.js.map