@nolans01/agent-validator 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,1804 @@
1
+ // src/resolver.ts
2
+ import path2 from "path";
3
+ import fs from "fs";
4
+ import { glob } from "glob";
5
+ import { execa } from "execa";
6
+
7
+ // src/detector.ts
8
+ import path from "path";
9
+ var EXTENSION_MAP = {
10
+ ".ts": "typescript",
11
+ ".tsx": "typescript",
12
+ ".js": "javascript",
13
+ ".jsx": "javascript",
14
+ ".mjs": "javascript",
15
+ ".cjs": "javascript",
16
+ ".py": "python",
17
+ ".rb": "ruby",
18
+ ".go": "go",
19
+ ".rs": "rust"
20
+ };
21
+ var SOURCE_EXTENSIONS = Object.keys(EXTENSION_MAP);
22
+ function detectLanguage(filePath) {
23
+ const ext = path.extname(filePath).toLowerCase();
24
+ return EXTENSION_MAP[ext] ?? "unknown";
25
+ }
26
+ function detectPrimaryLanguage(files) {
27
+ const counts = /* @__PURE__ */ new Map();
28
+ for (const file of files) {
29
+ if (file.language === "unknown") continue;
30
+ counts.set(file.language, (counts.get(file.language) ?? 0) + 1);
31
+ }
32
+ let maxLang = "unknown";
33
+ let maxCount = 0;
34
+ for (const [lang, count] of counts) {
35
+ if (count > maxCount) {
36
+ maxCount = count;
37
+ maxLang = lang;
38
+ }
39
+ }
40
+ return maxLang;
41
+ }
42
+
43
+ // src/resolver.ts
44
+ async function resolveFiles(options) {
45
+ let rawPaths;
46
+ switch (options.mode) {
47
+ case "diff":
48
+ rawPaths = await resolveDiff(options);
49
+ break;
50
+ case "files":
51
+ rawPaths = await resolveFileList(options);
52
+ break;
53
+ case "dir":
54
+ rawPaths = await resolveDirectory(options);
55
+ break;
56
+ case "scan":
57
+ rawPaths = await resolveDirectory({ ...options, targets: ["."] });
58
+ break;
59
+ default:
60
+ throw new Error(`Unknown mode: ${options.mode}`);
61
+ }
62
+ const sourceFiles = rawPaths.filter((p) => {
63
+ const ext = path2.extname(p).toLowerCase();
64
+ return SOURCE_EXTENSIONS.includes(ext);
65
+ });
66
+ return sourceFiles.map((p) => ({
67
+ path: path2.resolve(options.workdir, p),
68
+ relativePath: p,
69
+ language: detectLanguage(p)
70
+ }));
71
+ }
72
+ async function resolveDiff(options) {
73
+ const args = ["diff", "--name-only", "--diff-filter=ACMR"];
74
+ if (options.staged) {
75
+ args.push("--staged");
76
+ } else {
77
+ const base = options.base ?? "main";
78
+ const head = options.head ?? "HEAD";
79
+ args.push(`${base}...${head}`);
80
+ }
81
+ const result = await execa("git", args, { cwd: options.workdir });
82
+ return result.stdout.trim().split("\n").filter(Boolean);
83
+ }
84
+ async function resolveFileList(options) {
85
+ const targets = options.targets ?? [];
86
+ const resolved = [];
87
+ for (const target of targets) {
88
+ if (target.includes("*") || target.includes("?")) {
89
+ const matches = await glob(target, { cwd: options.workdir });
90
+ resolved.push(...matches);
91
+ } else {
92
+ const fullPath = path2.resolve(options.workdir, target);
93
+ if (fs.existsSync(fullPath)) {
94
+ resolved.push(target);
95
+ }
96
+ }
97
+ }
98
+ return resolved;
99
+ }
100
+ async function resolveDirectory(options) {
101
+ const dirs = options.targets ?? ["."];
102
+ const patterns = dirs.map((d) => `${d}/**/*`);
103
+ const defaultExclude = ["**/node_modules/**", "**/dist/**", "**/.git/**"];
104
+ const ignore = [...defaultExclude, ...options.exclude ?? []];
105
+ return glob(patterns, {
106
+ cwd: options.workdir,
107
+ nodir: true,
108
+ ignore
109
+ });
110
+ }
111
+
112
+ // src/adapters/tsc.ts
113
+ import { execa as execa3 } from "execa";
114
+ import fs2 from "fs";
115
+ import path3 from "path";
116
+
117
+ // src/adapters/base.ts
118
+ import { execa as execa2 } from "execa";
119
+ async function isBinaryAvailable(command) {
120
+ try {
121
+ await execa2(command, ["--version"]);
122
+ return true;
123
+ } catch {
124
+ return false;
125
+ }
126
+ }
127
+
128
+ // src/adapters/tsc.ts
129
+ var TSC_LINE_RE = /^(.+)\((\d+),(\d+)\): (error|warning) (TS\d+): (.+)$/;
130
+ function resolveLocalBinary(name, workdir) {
131
+ const localPath = path3.join(workdir, "node_modules", ".bin", name);
132
+ return fs2.existsSync(localPath) ? localPath : void 0;
133
+ }
134
+ function buildTscArgs(config, tsconfigPath, hasTsconfig, files) {
135
+ const args = ["--noEmit", "--pretty", "false"];
136
+ if (hasTsconfig) {
137
+ args.push("--project", tsconfigPath);
138
+ } else {
139
+ if (config.strict) args.push("--strict");
140
+ args.push(...files);
141
+ }
142
+ return args;
143
+ }
144
+ function parseTscOutput(stdout, workdir, fileSet, hasTsconfig) {
145
+ const findings = [];
146
+ for (const line of stdout.split("\n")) {
147
+ const match = line.match(TSC_LINE_RE);
148
+ if (!match) continue;
149
+ const [, filePath, lineNum, , severity, code, message] = match;
150
+ const relativePath = filePath.startsWith("/") ? path3.relative(workdir, filePath) : filePath;
151
+ if (hasTsconfig && !fileSet.has(relativePath)) continue;
152
+ findings.push({
153
+ file: relativePath,
154
+ line: parseInt(lineNum, 10),
155
+ severity: severity === "error" ? "blocker" : "warning",
156
+ metric: "type_error",
157
+ message,
158
+ why: `TypeScript compiler error ${code}: the code will not compile.`,
159
+ suggestion: "Fix the type error to ensure type safety.",
160
+ metadata: { code, source: "tsc" }
161
+ });
162
+ }
163
+ return findings;
164
+ }
165
+ var TscAdapter = class {
166
+ name = "tsc";
167
+ supportedLanguages = ["typescript"];
168
+ async isAvailable(workdir) {
169
+ if (workdir && resolveLocalBinary("tsc", workdir)) return true;
170
+ return isBinaryAvailable("tsc");
171
+ }
172
+ async run(files, config) {
173
+ if (files.length === 0) return { findings: [] };
174
+ const tsconfigPath = config.tsconfigPath ? path3.resolve(config.workdir, config.tsconfigPath) : path3.join(config.workdir, "tsconfig.json");
175
+ const hasTsconfig = fs2.existsSync(tsconfigPath);
176
+ const args = buildTscArgs(config, tsconfigPath, hasTsconfig, files);
177
+ const binary = resolveLocalBinary("tsc", config.workdir) ?? "tsc";
178
+ const result = await execa3(binary, args, {
179
+ cwd: config.workdir,
180
+ reject: false
181
+ });
182
+ const stdout = result.stdout || "";
183
+ if (!stdout.trim()) return { findings: [] };
184
+ const fileSet = new Set(files);
185
+ return { findings: parseTscOutput(stdout, config.workdir, fileSet, hasTsconfig) };
186
+ }
187
+ };
188
+
189
+ // src/adapters/mypy.ts
190
+ import { execa as execa4 } from "execa";
191
+ var MYPY_LINE_RE = /^(.+):(\d+): (error|warning|note): (.+?)(?:\s+\[(.+)\])?$/;
192
+ var MypyAdapter = class {
193
+ name = "mypy";
194
+ supportedLanguages = ["python"];
195
+ async isAvailable() {
196
+ return isBinaryAvailable("mypy");
197
+ }
198
+ async run(files, config) {
199
+ if (files.length === 0) return { findings: [] };
200
+ const args = ["--no-color-output", "--no-error-summary"];
201
+ if (config.strict) args.push("--strict");
202
+ args.push(...files);
203
+ const result = await execa4("mypy", args, {
204
+ cwd: config.workdir,
205
+ reject: false
206
+ });
207
+ const stdout = result.stdout || "";
208
+ if (!stdout.trim()) return { findings: [] };
209
+ const findings = [];
210
+ for (const line of stdout.split("\n")) {
211
+ const match = line.match(MYPY_LINE_RE);
212
+ if (!match) continue;
213
+ const [, filePath, lineNum, severity, message, code] = match;
214
+ findings.push({
215
+ file: filePath,
216
+ line: parseInt(lineNum, 10),
217
+ severity: mapMypySeverity(severity),
218
+ metric: "type_error",
219
+ message,
220
+ why: buildMypyWhy(severity, code),
221
+ suggestion: "Fix the type annotation or value to satisfy the type checker.",
222
+ metadata: { code: code ?? void 0, source: "mypy" }
223
+ });
224
+ }
225
+ return { findings };
226
+ }
227
+ };
228
+ function mapMypySeverity(level) {
229
+ switch (level) {
230
+ case "error":
231
+ return "blocker";
232
+ case "warning":
233
+ return "warning";
234
+ default:
235
+ return "info";
236
+ }
237
+ }
238
+ function buildMypyWhy(severity, code) {
239
+ const parts = [];
240
+ if (severity === "error") {
241
+ parts.push("mypy type error: the code has an incorrect type annotation or usage");
242
+ } else if (severity === "warning") {
243
+ parts.push("mypy warning: potential type issue detected");
244
+ } else {
245
+ parts.push("mypy note: additional type information");
246
+ }
247
+ if (code) parts.push(`[${code}]`);
248
+ return parts.join(" ");
249
+ }
250
+
251
+ // src/scorer.ts
252
+ function calculateComplexityScore(input) {
253
+ if (input.totalFunctions === 0) return 100;
254
+ const violationCount = input.findings.length;
255
+ const cleanCount = Math.max(0, input.totalFunctions - violationCount);
256
+ return Math.round(cleanCount / input.totalFunctions * 100);
257
+ }
258
+ function deriveStatus(score, findings) {
259
+ const hasBlockers = findings.some((f) => f.severity === "blocker");
260
+ if (hasBlockers) return "fail";
261
+ if (score < 70) return "fail";
262
+ if (score < 85) return "warn";
263
+ return "pass";
264
+ }
265
+ function calculateSecurityScore(input) {
266
+ let score = 100;
267
+ for (const finding of input.findings) {
268
+ switch (finding.severity) {
269
+ case "blocker":
270
+ score -= 25;
271
+ break;
272
+ case "warning":
273
+ score -= 10;
274
+ break;
275
+ case "info":
276
+ score -= 3;
277
+ break;
278
+ }
279
+ }
280
+ return Math.max(0, score);
281
+ }
282
+ function calculateTypeSafetyScore(input) {
283
+ let score = 100;
284
+ for (const finding of input.findings) {
285
+ switch (finding.severity) {
286
+ case "blocker":
287
+ score -= 10;
288
+ break;
289
+ case "warning":
290
+ score -= 5;
291
+ break;
292
+ case "info":
293
+ score -= 2;
294
+ break;
295
+ }
296
+ }
297
+ return Math.max(0, score);
298
+ }
299
+ function calculateArchitectureScore(input) {
300
+ let score = 100;
301
+ for (const finding of input.findings) {
302
+ switch (finding.severity) {
303
+ case "blocker":
304
+ score -= 15;
305
+ break;
306
+ case "warning":
307
+ score -= 7;
308
+ break;
309
+ case "info":
310
+ score -= 3;
311
+ break;
312
+ }
313
+ }
314
+ return Math.max(0, score);
315
+ }
316
+ function calculateTestQualityScore(input) {
317
+ return Math.round(Math.max(0, Math.min(100, input.mutationScore)));
318
+ }
319
+ function overallStatus(results) {
320
+ if (results.some((r) => r.status === "fail")) return "fail";
321
+ if (results.some((r) => r.status === "warn")) return "warn";
322
+ return "pass";
323
+ }
324
+
325
+ // src/config/defaults.ts
326
+ var DEFAULT_COMPLEXITY = {
327
+ cyclomatic: 10,
328
+ length: 40,
329
+ arguments: 4,
330
+ nesting: 3
331
+ };
332
+ var DEFAULT_SECURITY = {
333
+ semgrepRules: ["p/security-audit", "p/secrets"],
334
+ gitleaksEnabled: true
335
+ };
336
+ var DEFAULT_TYPE_SAFETY = {
337
+ strict: false,
338
+ mypyEnabled: true
339
+ };
340
+ var DEFAULT_JSCPD_EXCLUDE = [
341
+ "**/test/**",
342
+ "**/tests/**",
343
+ "**/__tests__/**",
344
+ "**/*.test.*",
345
+ "**/*.spec.*",
346
+ "**/docs/**",
347
+ "**/*.md",
348
+ "**/fixtures/**",
349
+ "**/mocks/**",
350
+ "**/node_modules/**",
351
+ "**/dist/**",
352
+ "**/vendor/**"
353
+ ];
354
+ var DEFAULT_ARCHITECTURE = {
355
+ madgeEnabled: true,
356
+ jscpdEnabled: true,
357
+ knipEnabled: true
358
+ };
359
+ var DEFAULT_TEST_QUALITY = {
360
+ strykerEnabled: true,
361
+ mutmutEnabled: true,
362
+ mutantEnabled: true,
363
+ mutationScoreThreshold: 80,
364
+ timeout: 3e5,
365
+ maxSurvivorFindings: 5
366
+ };
367
+
368
+ // src/gates/shared.ts
369
+ function toolMissingResult(gate, start, message, suggestion) {
370
+ return {
371
+ gate,
372
+ score: 0,
373
+ status: "skip",
374
+ duration_ms: Date.now() - start,
375
+ findings: [
376
+ {
377
+ file: "",
378
+ line: 0,
379
+ severity: "info",
380
+ metric: "tool_missing",
381
+ message,
382
+ why: `The ${gate} gate requires at least one tool to function.`,
383
+ suggestion
384
+ }
385
+ ]
386
+ };
387
+ }
388
+
389
+ // src/gates/type-safety.ts
390
+ var TypeSafetyGate = class {
391
+ name = "type_safety";
392
+ tsc = new TscAdapter();
393
+ mypy = new MypyAdapter();
394
+ async run(ctx) {
395
+ const start = Date.now();
396
+ const config = ctx.config.typeSafety ?? DEFAULT_TYPE_SAFETY;
397
+ if (ctx.language === "typescript") {
398
+ return this.runTsc(ctx, config, start);
399
+ }
400
+ if (ctx.language === "python" && config.mypyEnabled) {
401
+ return this.runMypy(ctx, config, start);
402
+ }
403
+ const suggestion = ctx.language === "python" ? "Enable mypyEnabled in profile or install mypy: pip install mypy" : "No type checker supported for this language yet.";
404
+ return toolMissingResult(this.name, start, `No type checker available for language: ${ctx.language}`, suggestion);
405
+ }
406
+ getFiles(ctx, adapter) {
407
+ return ctx.files.filter((f) => adapter.supportedLanguages.includes(f.language)).map((f) => f.relativePath);
408
+ }
409
+ emptyResult(start) {
410
+ return { gate: this.name, score: 100, status: "pass", duration_ms: Date.now() - start, findings: [] };
411
+ }
412
+ async runTsc(ctx, config, start) {
413
+ const available = await this.tsc.isAvailable(ctx.workdir);
414
+ if (!available) {
415
+ return toolMissingResult(this.name, start, "tsc is not installed", "Install with: npm install -g typescript");
416
+ }
417
+ const files = this.getFiles(ctx, this.tsc);
418
+ if (files.length === 0) return this.emptyResult(start);
419
+ const adapterConfig = {
420
+ workdir: ctx.workdir,
421
+ thresholds: {},
422
+ strict: config.strict,
423
+ tsconfigPath: config.tsconfigPath
424
+ };
425
+ const result = await this.tsc.run(files, adapterConfig);
426
+ return this.buildResult(result.findings, start);
427
+ }
428
+ async runMypy(ctx, config, start) {
429
+ const available = await this.mypy.isAvailable();
430
+ if (!available) {
431
+ return toolMissingResult(this.name, start, "mypy is not installed", "Install with: pip install mypy");
432
+ }
433
+ const files = this.getFiles(ctx, this.mypy);
434
+ if (files.length === 0) return this.emptyResult(start);
435
+ const adapterConfig = {
436
+ workdir: ctx.workdir,
437
+ thresholds: {},
438
+ strict: config.strict
439
+ };
440
+ const result = await this.mypy.run(files, adapterConfig);
441
+ return this.buildResult(result.findings, start);
442
+ }
443
+ buildResult(findings, start) {
444
+ const score = calculateTypeSafetyScore({ findings });
445
+ const status = deriveStatus(score, findings);
446
+ return { gate: this.name, score, status, duration_ms: Date.now() - start, findings };
447
+ }
448
+ };
449
+
450
+ // src/adapters/lizard.ts
451
+ import { execa as execa5 } from "execa";
452
+
453
+ // src/utils.ts
454
+ function chunk(arr, size) {
455
+ if (size <= 0) return [arr];
456
+ const chunks = [];
457
+ for (let i = 0; i < arr.length; i += size) {
458
+ chunks.push(arr.slice(i, i + size));
459
+ }
460
+ return chunks;
461
+ }
462
+
463
+ // src/adapters/lizard.ts
464
+ var LizardAdapter = class {
465
+ name = "lizard";
466
+ supportedLanguages = [
467
+ "typescript",
468
+ "javascript",
469
+ "python",
470
+ "ruby",
471
+ "go",
472
+ "rust"
473
+ ];
474
+ async isAvailable() {
475
+ return isBinaryAvailable("lizard");
476
+ }
477
+ async run(files, config) {
478
+ if (files.length === 0) {
479
+ return { findings: [], totalFunctions: 0 };
480
+ }
481
+ const allFindings = [];
482
+ let totalFunctions = 0;
483
+ for (const batch of chunk(files, 50)) {
484
+ const result = await execa5("lizard", [...batch, "--csv"], {
485
+ cwd: config.workdir,
486
+ reject: false
487
+ });
488
+ const parsed = parseLizardCsv(result.stdout, config.thresholds);
489
+ allFindings.push(...parsed.findings);
490
+ totalFunctions += parsed.totalFunctions;
491
+ }
492
+ return { findings: allFindings, totalFunctions };
493
+ }
494
+ };
495
+ function parseCsvRow(fields) {
496
+ if (fields.length < 11) return null;
497
+ const ccn = parseInt(fields[1], 10);
498
+ if (isNaN(ccn)) return null;
499
+ return {
500
+ nloc: parseInt(fields[0], 10),
501
+ ccn,
502
+ params: parseInt(fields[3], 10),
503
+ file: fields[6],
504
+ func: fields[7],
505
+ start: parseInt(fields[9], 10),
506
+ end: parseInt(fields[10], 10)
507
+ };
508
+ }
509
+ function detectViolations(m, thresholds) {
510
+ const violations = [];
511
+ if (m.ccn > (thresholds.cyclomatic ?? 10)) violations.push("cyclomatic");
512
+ if (m.nloc > (thresholds.length ?? 40)) violations.push("length");
513
+ if (m.params > (thresholds.arguments ?? 4)) violations.push("arguments");
514
+ return violations;
515
+ }
516
+ function metricsToFinding(m, violations, thresholds) {
517
+ return {
518
+ file: m.file,
519
+ line: m.start,
520
+ end_line: m.end,
521
+ function: m.func,
522
+ severity: determineSeverity(m, thresholds),
523
+ metric: "complexity",
524
+ value: m.ccn,
525
+ threshold: thresholds.cyclomatic ?? 10,
526
+ message: buildMessage(m, violations, thresholds),
527
+ why: buildWhy(m, violations),
528
+ suggestion: buildSuggestion(violations),
529
+ metadata: { nloc: m.nloc, ccn: m.ccn, params: m.params, violations }
530
+ };
531
+ }
532
+ function parseLizardCsv(csv, thresholds) {
533
+ const lines = csv.trim().split("\n");
534
+ if (lines.length <= 1) return { findings: [], totalFunctions: 0 };
535
+ const findings = [];
536
+ let totalFunctions = 0;
537
+ for (const line of lines.slice(1)) {
538
+ const m = parseCsvRow(parseCSVLine(line));
539
+ if (!m) continue;
540
+ totalFunctions++;
541
+ const violations = detectViolations(m, thresholds);
542
+ if (violations.length === 0) continue;
543
+ findings.push(metricsToFinding(m, violations, thresholds));
544
+ }
545
+ return { findings, totalFunctions };
546
+ }
547
+ function determineSeverity(m, thresholds) {
548
+ const ccnRatio = m.ccn / (thresholds.cyclomatic ?? 10);
549
+ const nlocRatio = m.nloc / (thresholds.length ?? 40);
550
+ if (ccnRatio > 1.5 || nlocRatio > 1.5) return "blocker";
551
+ return "warning";
552
+ }
553
+ function buildMessage(m, violations, thresholds) {
554
+ const parts = [];
555
+ if (violations.includes("cyclomatic")) {
556
+ parts.push(`Cyclomatic: ${m.ccn} (max: ${thresholds.cyclomatic ?? 10})`);
557
+ }
558
+ if (violations.includes("length")) {
559
+ parts.push(`NLOC: ${m.nloc} (max: ${thresholds.length ?? 40})`);
560
+ }
561
+ if (violations.includes("arguments")) {
562
+ parts.push(`Params: ${m.params} (max: ${thresholds.arguments ?? 4})`);
563
+ }
564
+ return parts.join(" | ");
565
+ }
566
+ function buildWhy(m, violations) {
567
+ if (violations.includes("cyclomatic") && violations.includes("length")) {
568
+ return `${m.ccn} execution paths in ${m.nloc} lines. Difficult to test exhaustively and high risk of bugs on change.`;
569
+ }
570
+ if (violations.includes("cyclomatic")) {
571
+ return `${m.ccn} execution paths make this function hard to test and maintain.`;
572
+ }
573
+ if (violations.includes("length")) {
574
+ return `${m.nloc} lines suggests this function does more than one thing.`;
575
+ }
576
+ if (violations.includes("arguments")) {
577
+ return "Too many parameters indicates this function has too many responsibilities or needs a config object.";
578
+ }
579
+ return "Function exceeds complexity thresholds.";
580
+ }
581
+ function buildSuggestion(violations) {
582
+ if (violations.includes("cyclomatic") || violations.includes("length")) {
583
+ return "Extract into smaller, focused functions with single responsibility.";
584
+ }
585
+ if (violations.includes("arguments")) {
586
+ return "Group related parameters into an options/config object.";
587
+ }
588
+ return "Simplify this function.";
589
+ }
590
+ function parseCSVLine(line) {
591
+ const fields = [];
592
+ let current = "";
593
+ let inQuotes = false;
594
+ for (const char of line) {
595
+ if (char === '"') {
596
+ inQuotes = !inQuotes;
597
+ } else if (char === "," && !inQuotes) {
598
+ fields.push(current.trim());
599
+ current = "";
600
+ } else {
601
+ current += char;
602
+ }
603
+ }
604
+ fields.push(current.trim());
605
+ return fields;
606
+ }
607
+
608
+ // src/gates/complexity.ts
609
+ var ComplexityGate = class {
610
+ name = "complexity";
611
+ adapter = new LizardAdapter();
612
+ async run(ctx) {
613
+ const start = Date.now();
614
+ const available = await this.adapter.isAvailable();
615
+ if (!available) {
616
+ return toolMissingResult(this.name, start, "lizard is not installed", "Install with: pip install lizard");
617
+ }
618
+ const supportedFiles = ctx.files.filter((f) => this.adapter.supportedLanguages.includes(f.language)).map((f) => f.relativePath);
619
+ if (supportedFiles.length === 0) {
620
+ return {
621
+ gate: this.name,
622
+ score: 100,
623
+ status: "pass",
624
+ duration_ms: Date.now() - start,
625
+ findings: []
626
+ };
627
+ }
628
+ const { findings, totalFunctions = 0 } = await this.adapter.run(supportedFiles, {
629
+ workdir: ctx.workdir,
630
+ thresholds: {
631
+ cyclomatic: ctx.config.complexity.cyclomatic,
632
+ length: ctx.config.complexity.length,
633
+ arguments: ctx.config.complexity.arguments
634
+ }
635
+ });
636
+ const score = calculateComplexityScore({ totalFunctions, findings });
637
+ const status = deriveStatus(score, findings);
638
+ const duration_ms = Date.now() - start;
639
+ return { gate: this.name, score, status, duration_ms, findings };
640
+ }
641
+ };
642
+
643
+ // src/adapters/semgrep.ts
644
+ import { execa as execa6 } from "execa";
645
+ var SemgrepAdapter = class {
646
+ name = "semgrep";
647
+ supportedLanguages = [
648
+ "typescript",
649
+ "javascript",
650
+ "python",
651
+ "ruby",
652
+ "go",
653
+ "rust"
654
+ ];
655
+ async isAvailable() {
656
+ return isBinaryAvailable("semgrep");
657
+ }
658
+ async run(files, config) {
659
+ if (files.length === 0) {
660
+ return { findings: [] };
661
+ }
662
+ const rules = config.rules ?? ["p/security-audit", "p/secrets"];
663
+ const allFindings = [];
664
+ for (const batch of chunk(files, 50)) {
665
+ const configArgs = rules.flatMap((r) => ["--config", r]);
666
+ const result = await execa6(
667
+ "semgrep",
668
+ [...configArgs, "--json", ...batch],
669
+ { cwd: config.workdir, reject: false }
670
+ );
671
+ if (result.stdout) {
672
+ const parsed = parseSemgrepJson(result.stdout);
673
+ allFindings.push(...parsed);
674
+ }
675
+ }
676
+ return { findings: allFindings };
677
+ }
678
+ };
679
+ function parseSemgrepJson(json) {
680
+ let output;
681
+ try {
682
+ output = JSON.parse(json);
683
+ } catch {
684
+ return [];
685
+ }
686
+ if (!output.results || !Array.isArray(output.results)) {
687
+ return [];
688
+ }
689
+ return output.results.map((r) => ({
690
+ file: r.path,
691
+ line: r.start.line,
692
+ end_line: r.end.line,
693
+ severity: mapSeverity(r.extra.severity),
694
+ metric: "security",
695
+ message: r.extra.message,
696
+ why: buildWhy2(r),
697
+ suggestion: r.extra.fix ?? void 0,
698
+ metadata: {
699
+ ruleId: r.check_id,
700
+ cwe: r.extra.metadata?.cwe,
701
+ owasp: r.extra.metadata?.owasp,
702
+ confidence: r.extra.metadata?.confidence,
703
+ source: "semgrep"
704
+ }
705
+ }));
706
+ }
707
+ function mapSeverity(severity) {
708
+ switch (severity) {
709
+ case "ERROR":
710
+ return "blocker";
711
+ case "WARNING":
712
+ return "warning";
713
+ case "INFO":
714
+ return "info";
715
+ default:
716
+ return "warning";
717
+ }
718
+ }
719
+ function buildWhy2(result) {
720
+ const parts = [];
721
+ if (result.extra.metadata?.cwe?.length) {
722
+ parts.push(result.extra.metadata.cwe.join(", "));
723
+ }
724
+ if (result.extra.metadata?.owasp?.length) {
725
+ parts.push(result.extra.metadata.owasp.join(", "));
726
+ }
727
+ if (parts.length === 0) {
728
+ return `Security issue detected by rule ${result.check_id}.`;
729
+ }
730
+ return `${parts.join(" | ")}. Detected by rule ${result.check_id}.`;
731
+ }
732
+
733
+ // src/adapters/gitleaks.ts
734
+ import { execa as execa7 } from "execa";
735
+ var GitleaksAdapter = class {
736
+ name = "gitleaks";
737
+ supportedLanguages = [
738
+ "typescript",
739
+ "javascript",
740
+ "python",
741
+ "ruby",
742
+ "go",
743
+ "rust",
744
+ "unknown"
745
+ ];
746
+ async isAvailable() {
747
+ return isBinaryAvailable("gitleaks");
748
+ }
749
+ async run(files, config) {
750
+ const result = await execa7(
751
+ "gitleaks",
752
+ ["detect", "--source", config.workdir, "--no-git", "-f", "json", "--report-path", "/dev/stdout"],
753
+ { cwd: config.workdir, reject: false }
754
+ );
755
+ if (!result.stdout || result.stdout.trim() === "") {
756
+ return { findings: [] };
757
+ }
758
+ const allLeaks = parseGitleaksJson(result.stdout);
759
+ const fileSet = new Set(files);
760
+ const filtered = allLeaks.filter((f) => fileSet.has(f.file));
761
+ return { findings: filtered };
762
+ }
763
+ };
764
+ function parseGitleaksJson(json) {
765
+ let leaks;
766
+ try {
767
+ leaks = JSON.parse(json);
768
+ } catch {
769
+ return [];
770
+ }
771
+ if (!Array.isArray(leaks)) {
772
+ return [];
773
+ }
774
+ return leaks.map((leak) => ({
775
+ file: leak.File,
776
+ line: leak.StartLine,
777
+ end_line: leak.EndLine,
778
+ severity: "blocker",
779
+ metric: "security",
780
+ message: `Secret detected: ${leak.Description}`,
781
+ why: `Exposed secrets can lead to unauthorized access. Rule: ${leak.RuleID}.`,
782
+ suggestion: "Remove the secret and rotate the credential. Use environment variables or a secrets manager.",
783
+ metadata: {
784
+ ruleId: leak.RuleID,
785
+ entropy: leak.Entropy,
786
+ fingerprint: leak.Fingerprint,
787
+ source: "gitleaks"
788
+ }
789
+ }));
790
+ }
791
+
792
+ // src/gates/security.ts
793
+ var SecurityGate = class {
794
+ name = "security";
795
+ semgrep = new SemgrepAdapter();
796
+ gitleaks = new GitleaksAdapter();
797
+ async run(ctx) {
798
+ const start = Date.now();
799
+ const securityConfig = ctx.config.security ?? DEFAULT_SECURITY;
800
+ const semgrepAvailable = await this.semgrep.isAvailable();
801
+ const gitleaksAvailable = securityConfig.gitleaksEnabled ? await this.gitleaks.isAvailable() : false;
802
+ if (!semgrepAvailable && !gitleaksAvailable) {
803
+ return toolMissingResult(this.name, start, "Neither semgrep nor gitleaks is installed", "Install with: pip install semgrep OR brew install gitleaks");
804
+ }
805
+ const allFindings = [];
806
+ if (semgrepAvailable) {
807
+ const supportedFiles = ctx.files.filter((f) => this.semgrep.supportedLanguages.includes(f.language)).map((f) => f.relativePath);
808
+ if (supportedFiles.length > 0) {
809
+ const semgrepConfig = {
810
+ workdir: ctx.workdir,
811
+ thresholds: {},
812
+ rules: securityConfig.semgrepRules
813
+ };
814
+ const result = await this.semgrep.run(supportedFiles, semgrepConfig);
815
+ allFindings.push(...result.findings);
816
+ }
817
+ }
818
+ if (gitleaksAvailable) {
819
+ const allFiles = ctx.files.map((f) => f.relativePath);
820
+ const result = await this.gitleaks.run(allFiles, {
821
+ workdir: ctx.workdir,
822
+ thresholds: {}
823
+ });
824
+ allFindings.push(...result.findings);
825
+ }
826
+ const score = calculateSecurityScore({ findings: allFindings });
827
+ const status = deriveStatus(score, allFindings);
828
+ const duration_ms = Date.now() - start;
829
+ return { gate: this.name, score, status, duration_ms, findings: allFindings };
830
+ }
831
+ };
832
+
833
+ // src/adapters/madge.ts
834
+ import { execa as execa8 } from "execa";
835
+ function parseCycles(stdout) {
836
+ if (!stdout.trim()) return null;
837
+ try {
838
+ const cycles = JSON.parse(stdout);
839
+ if (!Array.isArray(cycles) || cycles.length === 0) return null;
840
+ return cycles;
841
+ } catch {
842
+ return null;
843
+ }
844
+ }
845
+ function cycleToFinding(cycle) {
846
+ const cycleDesc = cycle.join(" \u2192 ") + " \u2192 " + cycle[0];
847
+ return {
848
+ file: cycle[0],
849
+ line: 0,
850
+ severity: "blocker",
851
+ metric: "circular_dependency",
852
+ message: `Circular dependency: ${cycleDesc}`,
853
+ why: "Circular dependencies make the code harder to test, refactor, and reason about.",
854
+ suggestion: "Break the cycle by extracting shared code into a separate module.",
855
+ metadata: { cycle, source: "madge" }
856
+ };
857
+ }
858
+ var MadgeAdapter = class {
859
+ name = "madge";
860
+ supportedLanguages = ["typescript", "javascript"];
861
+ async isAvailable() {
862
+ return isBinaryAvailable("madge");
863
+ }
864
+ async run(files, config) {
865
+ if (files.length === 0) return { findings: [] };
866
+ const result = await execa8("madge", ["--circular", "--json", config.workdir], {
867
+ cwd: config.workdir,
868
+ reject: false
869
+ });
870
+ const cycles = parseCycles(result.stdout || "");
871
+ if (!cycles) return { findings: [] };
872
+ const fileSet = new Set(files);
873
+ const findings = cycles.filter((cycle) => Array.isArray(cycle) && cycle.length > 0).filter((cycle) => cycle.some((f) => fileSet.has(f))).map(cycleToFinding);
874
+ return { findings };
875
+ }
876
+ };
877
+
878
+ // src/adapters/jscpd.ts
879
+ import { execa as execa9 } from "execa";
880
+ import fs3 from "fs";
881
+ import os from "os";
882
+ import path4 from "path";
883
+ function buildArgs(config, tmpDir) {
884
+ const minLines = config.minLines ?? 5;
885
+ const minTokens = config.minTokens ?? 50;
886
+ const exclude = config.exclude ?? [];
887
+ const args = [
888
+ "--min-lines",
889
+ String(minLines),
890
+ "--min-tokens",
891
+ String(minTokens),
892
+ "--reporters",
893
+ "json",
894
+ "--silent",
895
+ "--output",
896
+ tmpDir
897
+ ];
898
+ for (const pattern of exclude) {
899
+ args.push("--ignore", pattern);
900
+ }
901
+ args.push(config.workdir);
902
+ return args;
903
+ }
904
+ function parseReport(reportPath) {
905
+ if (!fs3.existsSync(reportPath)) return null;
906
+ const raw = fs3.readFileSync(reportPath, "utf-8");
907
+ try {
908
+ const report = JSON.parse(raw);
909
+ if (!report.duplicates || !Array.isArray(report.duplicates)) return null;
910
+ return report;
911
+ } catch {
912
+ return null;
913
+ }
914
+ }
915
+ function cloneToFinding(clone, workdir) {
916
+ const firstRel = path4.relative(workdir, clone.firstFile.name);
917
+ const secondRel = path4.relative(workdir, clone.secondFile.name);
918
+ return {
919
+ file: firstRel,
920
+ line: clone.firstFile.startLoc.line,
921
+ end_line: clone.firstFile.endLoc.line,
922
+ severity: "warning",
923
+ metric: "code_duplication",
924
+ message: `${clone.lines} lines duplicated with ${secondRel}:${clone.secondFile.startLoc.line}`,
925
+ why: "Duplicated code increases maintenance burden and risk of inconsistent changes.",
926
+ suggestion: "Extract the duplicated logic into a shared function or module.",
927
+ metadata: {
928
+ lines: clone.lines,
929
+ tokens: clone.tokens,
930
+ secondFile: secondRel,
931
+ secondLine: clone.secondFile.startLoc.line,
932
+ source: "jscpd"
933
+ }
934
+ };
935
+ }
936
+ var JscpdAdapter = class {
937
+ name = "jscpd";
938
+ supportedLanguages = ["typescript", "javascript", "python", "ruby"];
939
+ async isAvailable() {
940
+ return isBinaryAvailable("jscpd");
941
+ }
942
+ async run(files, config) {
943
+ if (files.length === 0) return { findings: [] };
944
+ const jscpdConfig = config;
945
+ const tmpDir = fs3.mkdtempSync(path4.join(os.tmpdir(), "jscpd-"));
946
+ try {
947
+ const args = buildArgs(jscpdConfig, tmpDir);
948
+ await execa9("jscpd", args, { cwd: config.workdir, reject: false });
949
+ const report = parseReport(path4.join(tmpDir, "jscpd-report.json"));
950
+ if (!report) return { findings: [] };
951
+ const fileSet = new Set(files);
952
+ return {
953
+ findings: report.duplicates.filter((clone) => {
954
+ const firstRel = path4.relative(config.workdir, clone.firstFile.name);
955
+ const secondRel = path4.relative(config.workdir, clone.secondFile.name);
956
+ return fileSet.has(firstRel) || fileSet.has(secondRel);
957
+ }).map((clone) => cloneToFinding(clone, config.workdir))
958
+ };
959
+ } finally {
960
+ fs3.rmSync(tmpDir, { recursive: true, force: true });
961
+ }
962
+ }
963
+ };
964
+
965
+ // src/adapters/knip.ts
966
+ import { execa as execa10 } from "execa";
967
+ function collectUnusedFiles(report, fileSet) {
968
+ if (!report.files || !Array.isArray(report.files)) return [];
969
+ return report.files.filter((file) => fileSet.has(file)).map((file) => ({
970
+ file,
971
+ line: 0,
972
+ severity: "info",
973
+ metric: "dead_code",
974
+ message: `Unused file: ${file}`,
975
+ why: "Unused files add confusion and increase bundle/maintenance cost.",
976
+ suggestion: "Remove the file if it is no longer needed.",
977
+ metadata: { type: "file", source: "knip" }
978
+ }));
979
+ }
980
+ function exportToFinding(file, exp) {
981
+ return {
982
+ file,
983
+ line: exp.line ?? 0,
984
+ severity: "info",
985
+ metric: "unused_export",
986
+ message: `Unused export: ${exp.name}`,
987
+ why: "Unused exports indicate dead code that may confuse consumers.",
988
+ suggestion: `Remove the export or make '${exp.name}' internal.`,
989
+ metadata: { type: "export", exportName: exp.name, source: "knip" }
990
+ };
991
+ }
992
+ function typeToFinding(file, typ) {
993
+ return {
994
+ file,
995
+ line: typ.line ?? 0,
996
+ severity: "info",
997
+ metric: "unused_export",
998
+ message: `Unused exported type: ${typ.name}`,
999
+ why: "Unused type exports indicate dead code.",
1000
+ suggestion: `Remove the type export '${typ.name}' if no longer needed.`,
1001
+ metadata: { type: "type", exportName: typ.name, source: "knip" }
1002
+ };
1003
+ }
1004
+ function collectUnusedExports(report, fileSet) {
1005
+ if (!report.issues || !Array.isArray(report.issues)) return [];
1006
+ const findings = [];
1007
+ for (const issue of report.issues) {
1008
+ if (!fileSet.has(issue.file)) continue;
1009
+ if (issue.exports) findings.push(...issue.exports.map((e) => exportToFinding(issue.file, e)));
1010
+ if (issue.types) findings.push(...issue.types.map((t) => typeToFinding(issue.file, t)));
1011
+ }
1012
+ return findings;
1013
+ }
1014
+ var KnipAdapter = class {
1015
+ name = "knip";
1016
+ supportedLanguages = ["typescript"];
1017
+ async isAvailable() {
1018
+ return isBinaryAvailable("knip");
1019
+ }
1020
+ async run(files, config) {
1021
+ if (files.length === 0) return { findings: [] };
1022
+ const result = await execa10("knip", ["--reporter", "json"], {
1023
+ cwd: config.workdir,
1024
+ reject: false
1025
+ });
1026
+ const stdout = result.stdout || "";
1027
+ if (!stdout.trim()) return { findings: [] };
1028
+ let report;
1029
+ try {
1030
+ report = JSON.parse(stdout);
1031
+ } catch {
1032
+ return { findings: [] };
1033
+ }
1034
+ const fileSet = new Set(files);
1035
+ return {
1036
+ findings: [
1037
+ ...collectUnusedFiles(report, fileSet),
1038
+ ...collectUnusedExports(report, fileSet)
1039
+ ]
1040
+ };
1041
+ }
1042
+ };
1043
+
1044
+ // src/gates/architecture.ts
1045
+ var ArchitectureGate = class {
1046
+ name = "architecture";
1047
+ madge = new MadgeAdapter();
1048
+ jscpd = new JscpdAdapter();
1049
+ knip = new KnipAdapter();
1050
+ async run(ctx) {
1051
+ const start = Date.now();
1052
+ const config = ctx.config.architecture ?? DEFAULT_ARCHITECTURE;
1053
+ const available = await this.checkAvailability(config, ctx);
1054
+ if (!available.madge && !available.jscpd && !available.knip) {
1055
+ return toolMissingResult(this.name, start, "No architecture tools available", "Install with: npm install -g madge jscpd knip");
1056
+ }
1057
+ const allFindings = await this.collectFindings(available, config, ctx);
1058
+ const score = calculateArchitectureScore({ findings: allFindings });
1059
+ const status = deriveStatus(score, allFindings);
1060
+ return { gate: this.name, score, status, duration_ms: Date.now() - start, findings: allFindings };
1061
+ }
1062
+ async checkAvailability(config, ctx) {
1063
+ const madgeApplicable = config.madgeEnabled && this.madge.supportedLanguages.includes(ctx.language);
1064
+ const jscpdApplicable = config.jscpdEnabled;
1065
+ const knipApplicable = config.knipEnabled && this.knip.supportedLanguages.includes(ctx.language);
1066
+ return {
1067
+ madge: madgeApplicable ? await this.madge.isAvailable() : false,
1068
+ jscpd: jscpdApplicable ? await this.jscpd.isAvailable() : false,
1069
+ knip: knipApplicable ? await this.knip.isAvailable() : false
1070
+ };
1071
+ }
1072
+ async collectFindings(available, config, ctx) {
1073
+ const allFindings = [];
1074
+ if (available.madge) {
1075
+ const findings = await this.runAdapter(this.madge, ctx, { workdir: ctx.workdir, thresholds: {} });
1076
+ allFindings.push(...findings);
1077
+ }
1078
+ if (available.jscpd) {
1079
+ const jscpdConfig = {
1080
+ workdir: ctx.workdir,
1081
+ thresholds: {},
1082
+ minLines: config.jscpdMinLines,
1083
+ minTokens: config.jscpdMinTokens,
1084
+ exclude: config.jscpdExclude ?? DEFAULT_JSCPD_EXCLUDE
1085
+ };
1086
+ const findings = await this.runAdapter(this.jscpd, ctx, jscpdConfig);
1087
+ allFindings.push(...findings);
1088
+ }
1089
+ if (available.knip) {
1090
+ const findings = await this.runAdapter(this.knip, ctx, { workdir: ctx.workdir, thresholds: {} });
1091
+ allFindings.push(...findings);
1092
+ }
1093
+ return allFindings;
1094
+ }
1095
+ async runAdapter(adapter, ctx, config) {
1096
+ const files = ctx.files.filter((f) => adapter.supportedLanguages.includes(f.language)).map((f) => f.relativePath);
1097
+ if (files.length === 0) return [];
1098
+ const result = await adapter.run(files, config);
1099
+ return result.findings;
1100
+ }
1101
+ };
1102
+
1103
+ // src/adapters/stryker.ts
1104
+ import { execa as execa11 } from "execa";
1105
+ import fs4 from "fs";
1106
+ import path5 from "path";
1107
+
1108
+ // src/adapters/mutation-shared.ts
1109
+ function scoreSeverity(score) {
1110
+ if (score < 40) return "blocker";
1111
+ if (score < 70) return "warning";
1112
+ return "info";
1113
+ }
1114
+ function buildScoreMessage(score, threshold, survived, total) {
1115
+ return "Mutation score " + Math.round(score) + "% is below threshold " + threshold + "% (" + survived + " survived of " + total + ")";
1116
+ }
1117
+ function timeoutResult(tool, timeout) {
1118
+ return {
1119
+ findings: [{
1120
+ file: "",
1121
+ line: 0,
1122
+ severity: "info",
1123
+ metric: "mutation_timeout",
1124
+ message: tool + " timed out after " + timeout + "ms",
1125
+ why: "Mutation testing exceeded the configured timeout.",
1126
+ suggestion: "Increase the timeout or reduce the number of files to mutate."
1127
+ }],
1128
+ mutationScore: 0,
1129
+ timedOut: true
1130
+ };
1131
+ }
1132
+ function fileScoreFinding(input) {
1133
+ return {
1134
+ file: input.file,
1135
+ line: 0,
1136
+ severity: scoreSeverity(input.score),
1137
+ metric: "mutation_score",
1138
+ value: Math.round(input.score),
1139
+ threshold: input.threshold,
1140
+ message: buildScoreMessage(input.score, input.threshold, input.survived, input.total),
1141
+ why: "A low mutation score means tests do not detect code changes, indicating weak or missing test coverage.",
1142
+ suggestion: "Add or improve tests for this file to kill surviving mutants.",
1143
+ metadata: { killed: input.killed, survived: input.survived, source: input.source }
1144
+ };
1145
+ }
1146
+
1147
+ // src/adapters/stryker.ts
1148
+ function calculateFileScore(mutants) {
1149
+ let killed = 0;
1150
+ let survived = 0;
1151
+ let noCoverage = 0;
1152
+ let timeout = 0;
1153
+ let invalid = 0;
1154
+ for (const m of mutants) {
1155
+ switch (m.status) {
1156
+ case "Killed":
1157
+ killed++;
1158
+ break;
1159
+ case "Survived":
1160
+ survived++;
1161
+ break;
1162
+ case "NoCoverage":
1163
+ noCoverage++;
1164
+ break;
1165
+ case "Timeout":
1166
+ timeout++;
1167
+ break;
1168
+ case "CompileError":
1169
+ case "RuntimeError":
1170
+ invalid++;
1171
+ break;
1172
+ }
1173
+ }
1174
+ const total = mutants.length - invalid;
1175
+ const score = total > 0 ? (killed + timeout) / total * 100 : 100;
1176
+ return { killed, survived, noCoverage, timeout, total, score };
1177
+ }
1178
+ function scoreSeverity2(score) {
1179
+ if (score < 40) return "blocker";
1180
+ if (score < 70) return "warning";
1181
+ return "info";
1182
+ }
1183
+ function buildScoreMessage2(score, threshold, survived, noCoverage) {
1184
+ return "Mutation score " + Math.round(score) + "% is below threshold " + threshold + "% (" + survived + " survived, " + noCoverage + " no coverage)";
1185
+ }
1186
+ function fileScoreToFinding(fileScore, threshold) {
1187
+ const severity = scoreSeverity2(fileScore.score);
1188
+ return {
1189
+ file: fileScore.file,
1190
+ line: 0,
1191
+ severity,
1192
+ metric: "mutation_score",
1193
+ value: Math.round(fileScore.score),
1194
+ threshold,
1195
+ message: buildScoreMessage2(fileScore.score, threshold, fileScore.survived, fileScore.noCoverage),
1196
+ why: "A low mutation score means tests do not detect code changes, indicating weak or missing test coverage.",
1197
+ suggestion: "Add or improve tests for this file to kill surviving mutants.",
1198
+ metadata: { killed: fileScore.killed, survived: fileScore.survived, noCoverage: fileScore.noCoverage, source: "stryker" }
1199
+ };
1200
+ }
1201
+ function formatMutantMessage(mutant) {
1202
+ const base = "Surviving mutant: " + mutant.mutatorName;
1203
+ return mutant.replacement ? base + " \u2192 " + mutant.replacement : base;
1204
+ }
1205
+ function survivorToFinding(file, mutant) {
1206
+ return {
1207
+ file,
1208
+ line: mutant.location.start.line,
1209
+ end_line: mutant.location.end.line,
1210
+ severity: "info",
1211
+ metric: "surviving_mutant",
1212
+ message: formatMutantMessage(mutant),
1213
+ why: "No test detects this code change, meaning this logic path is not properly verified.",
1214
+ suggestion: "Add a test that would fail if this mutation were applied.",
1215
+ metadata: { mutatorName: mutant.mutatorName, status: mutant.status, source: "stryker" }
1216
+ };
1217
+ }
1218
+ function findReportPath(workdir) {
1219
+ const candidates = [
1220
+ path5.join(workdir, "reports", "mutation", "mutation.json"),
1221
+ path5.join(workdir, "reports", "mutation.json")
1222
+ ];
1223
+ return candidates.find((p) => fs4.existsSync(p));
1224
+ }
1225
+ function readReport(workdir) {
1226
+ const reportPath = findReportPath(workdir);
1227
+ if (!reportPath) return null;
1228
+ try {
1229
+ const raw = fs4.readFileSync(reportPath, "utf-8");
1230
+ return JSON.parse(raw);
1231
+ } catch {
1232
+ return null;
1233
+ }
1234
+ }
1235
+ function processReport(report, files, threshold, maxSurvivorFindings) {
1236
+ const fileSet = new Set(files);
1237
+ const findings = [];
1238
+ let totalKilled = 0;
1239
+ let totalValid = 0;
1240
+ for (const [filePath, fileResult] of Object.entries(report.files)) {
1241
+ if (!fileSet.has(filePath)) continue;
1242
+ const stats = calculateFileScore(fileResult.mutants);
1243
+ totalKilled += stats.killed + stats.timeout;
1244
+ totalValid += stats.total;
1245
+ if (stats.score < threshold) {
1246
+ const fileScore = { file: filePath, ...stats };
1247
+ findings.push(fileScoreToFinding(fileScore, threshold));
1248
+ const survivors = fileResult.mutants.filter((m) => m.status === "Survived" || m.status === "NoCoverage").slice(0, maxSurvivorFindings);
1249
+ findings.push(...survivors.map((m) => survivorToFinding(filePath, m)));
1250
+ }
1251
+ }
1252
+ const mutationScore = totalValid > 0 ? totalKilled / totalValid * 100 : 100;
1253
+ return { findings, mutationScore };
1254
+ }
1255
+ var StrykerAdapter = class {
1256
+ name = "stryker";
1257
+ supportedLanguages = ["typescript", "javascript"];
1258
+ async isAvailable(workdir) {
1259
+ if (workdir) {
1260
+ const localBin = path5.join(workdir, "node_modules", ".bin", "stryker");
1261
+ if (fs4.existsSync(localBin)) return true;
1262
+ }
1263
+ return await isBinaryAvailable("stryker");
1264
+ }
1265
+ async run(files, config) {
1266
+ if (files.length === 0) return { findings: [], mutationScore: 100 };
1267
+ const timeout = config.timeout ?? 3e5;
1268
+ const timedOut = await this.executeStryker(files, config, timeout);
1269
+ if (timedOut) return timeoutResult("Stryker", timeout);
1270
+ const report = readReport(config.workdir);
1271
+ if (!report) return { findings: [], mutationScore: 100 };
1272
+ return processReport(report, files, config.mutationScoreThreshold ?? 80, config.maxSurvivorFindings ?? 5);
1273
+ }
1274
+ async executeStryker(files, config, timeout) {
1275
+ const mutatePattern = files.join(",");
1276
+ const args = ["run", "--reporters", "json", "--mutate", mutatePattern];
1277
+ const useNpx = fs4.existsSync(path5.join(config.workdir, "node_modules", ".bin", "stryker"));
1278
+ const command = useNpx ? "npx" : "stryker";
1279
+ const execArgs = useNpx ? ["stryker", ...args] : args;
1280
+ const result = await execa11(command, execArgs, {
1281
+ cwd: config.workdir,
1282
+ reject: false,
1283
+ timeout
1284
+ });
1285
+ return !!result.timedOut;
1286
+ }
1287
+ };
1288
+
1289
+ // src/adapters/mutmut.ts
1290
+ import { execa as execa12 } from "execa";
1291
+ function matchToResult(match) {
1292
+ const classname = match[1];
1293
+ const name = match[2];
1294
+ const closingType = match[3];
1295
+ const body = match[4] ?? "";
1296
+ const isSelfClosing = closingType.trim().startsWith("/");
1297
+ const survived = !isSelfClosing && body.includes("<failure");
1298
+ const file = classname.replace(/\./g, "/") + ".py";
1299
+ const lineMatch = name.match(/line\s+(\d+)/i) ?? name.match(/mutant\s+(\d+)/i);
1300
+ const line = lineMatch ? parseInt(lineMatch[1], 10) : void 0;
1301
+ return { file, name, status: survived ? "survived" : "killed", line };
1302
+ }
1303
+ function parseJunitXml(xml) {
1304
+ const results = [];
1305
+ const testcaseRe = /<testcase\s+classname="([^"]*)"[^>]*?name="([^"]*)"[^>]*?(\/\s*>|>([\s\S]*?)<\/testcase>)/g;
1306
+ let match;
1307
+ while ((match = testcaseRe.exec(xml)) !== null) {
1308
+ results.push(matchToResult(match));
1309
+ }
1310
+ return results;
1311
+ }
1312
+ function groupByFile(results) {
1313
+ const groups = /* @__PURE__ */ new Map();
1314
+ for (const r of results) {
1315
+ const group = groups.get(r.file) ?? { killed: 0, survived: 0, survivors: [] };
1316
+ if (r.status === "killed") {
1317
+ group.killed++;
1318
+ } else {
1319
+ group.survived++;
1320
+ group.survivors.push(r);
1321
+ }
1322
+ groups.set(r.file, group);
1323
+ }
1324
+ const fileStats = /* @__PURE__ */ new Map();
1325
+ for (const [file, g] of groups) {
1326
+ const total = g.killed + g.survived;
1327
+ const score = total > 0 ? g.killed / total * 100 : 100;
1328
+ fileStats.set(file, { killed: g.killed, survived: g.survived, total, score, survivors: g.survivors });
1329
+ }
1330
+ return fileStats;
1331
+ }
1332
+ function fileStatsToFindings(file, stats, threshold, maxSurvivorFindings) {
1333
+ const findings = [fileScoreFinding({ file, score: stats.score, threshold, survived: stats.survived, total: stats.total, killed: stats.killed, source: "mutmut" })];
1334
+ for (const survivor of stats.survivors.slice(0, maxSurvivorFindings)) {
1335
+ findings.push({
1336
+ file,
1337
+ line: survivor.line ?? 0,
1338
+ severity: "info",
1339
+ metric: "surviving_mutant",
1340
+ message: "Surviving mutant: " + survivor.name,
1341
+ why: "No test detects this code change, meaning this logic path is not properly verified.",
1342
+ suggestion: "Add a test that would fail if this mutation were applied.",
1343
+ metadata: { mutantName: survivor.name, source: "mutmut" }
1344
+ });
1345
+ }
1346
+ return findings;
1347
+ }
1348
+ var MutmutAdapter = class {
1349
+ name = "mutmut";
1350
+ supportedLanguages = ["python"];
1351
+ async isAvailable() {
1352
+ return isBinaryAvailable("mutmut");
1353
+ }
1354
+ async run(files, config) {
1355
+ if (files.length === 0) return { findings: [], mutationScore: 100 };
1356
+ const timeout = config.timeout ?? 3e5;
1357
+ const xml = await this.executeMutmut(files, config, timeout);
1358
+ if (xml === null) return timeoutResult("mutmut", timeout);
1359
+ if (!xml.trim()) return { findings: [], mutationScore: 100 };
1360
+ return this.processXml(xml, files, config.mutationScoreThreshold ?? 80, config.maxSurvivorFindings ?? 5);
1361
+ }
1362
+ async executeMutmut(files, config, timeout) {
1363
+ const pathsToMutate = files.join(",");
1364
+ const runResult = await execa12("mutmut", ["run", `--paths-to-mutate=${pathsToMutate}`, "--CI", "--no-progress"], {
1365
+ cwd: config.workdir,
1366
+ reject: false,
1367
+ timeout
1368
+ });
1369
+ if (runResult.timedOut) return null;
1370
+ const xmlResult = await execa12("mutmut", ["junitxml"], {
1371
+ cwd: config.workdir,
1372
+ reject: false
1373
+ });
1374
+ return xmlResult.stdout || "";
1375
+ }
1376
+ processXml(xml, files, threshold, maxSurvivorFindings) {
1377
+ const mutantResults = parseJunitXml(xml);
1378
+ if (mutantResults.length === 0) return { findings: [], mutationScore: 100 };
1379
+ const fileStatsMap = groupByFile(mutantResults);
1380
+ const fileSet = new Set(files);
1381
+ const findings = [];
1382
+ let totalKilled = 0;
1383
+ let totalMutants = 0;
1384
+ for (const [file, stats] of fileStatsMap) {
1385
+ if (!fileSet.has(file)) continue;
1386
+ totalKilled += stats.killed;
1387
+ totalMutants += stats.total;
1388
+ if (stats.score < threshold) {
1389
+ findings.push(...fileStatsToFindings(file, stats, threshold, maxSurvivorFindings));
1390
+ }
1391
+ }
1392
+ const mutationScore = totalMutants > 0 ? totalKilled / totalMutants * 100 : 100;
1393
+ return { findings, mutationScore };
1394
+ }
1395
+ };
1396
+
1397
+ // src/adapters/mutant.ts
1398
+ import { execa as execa13 } from "execa";
1399
+ var RESULT_LINE_RE = /^(alive|killed|timeout):(.+):(.+):(\d+)/;
1400
+ var COVERAGE_RE = /Coverage:\s+([\d.]+)%/;
1401
+ function parseOutput(stdout) {
1402
+ const entries = [];
1403
+ let overallScore = null;
1404
+ for (const line of stdout.split("\n")) {
1405
+ const resultMatch = RESULT_LINE_RE.exec(line);
1406
+ if (resultMatch) {
1407
+ entries.push({
1408
+ status: resultMatch[1],
1409
+ subject: resultMatch[2],
1410
+ file: resultMatch[3],
1411
+ line: parseInt(resultMatch[4], 10)
1412
+ });
1413
+ continue;
1414
+ }
1415
+ const coverageMatch = COVERAGE_RE.exec(line);
1416
+ if (coverageMatch) {
1417
+ overallScore = parseFloat(coverageMatch[1]);
1418
+ }
1419
+ }
1420
+ return { entries, overallScore };
1421
+ }
1422
+ function groupByFile2(entries) {
1423
+ const groups = /* @__PURE__ */ new Map();
1424
+ for (const entry of entries) {
1425
+ const group = groups.get(entry.file) ?? { killed: 0, survived: 0, timeout: 0, survivors: [] };
1426
+ switch (entry.status) {
1427
+ case "killed":
1428
+ group.killed++;
1429
+ break;
1430
+ case "alive":
1431
+ group.survived++;
1432
+ group.survivors.push(entry);
1433
+ break;
1434
+ case "timeout":
1435
+ group.timeout++;
1436
+ break;
1437
+ }
1438
+ groups.set(entry.file, group);
1439
+ }
1440
+ const fileStats = /* @__PURE__ */ new Map();
1441
+ for (const [file, g] of groups) {
1442
+ const total = g.killed + g.survived + g.timeout;
1443
+ const score = total > 0 ? (g.killed + g.timeout) / total * 100 : 100;
1444
+ fileStats.set(file, { killed: g.killed, survived: g.survived, timeout: g.timeout, total, score, survivors: g.survivors });
1445
+ }
1446
+ return fileStats;
1447
+ }
1448
+ function fileStatsToFinding(file, stats, threshold) {
1449
+ return fileScoreFinding({ file, score: stats.score, threshold, survived: stats.survived, total: stats.total, killed: stats.killed, source: "mutant" });
1450
+ }
1451
+ function survivorToFinding2(entry) {
1452
+ return {
1453
+ file: entry.file,
1454
+ line: entry.line,
1455
+ severity: "info",
1456
+ metric: "surviving_mutant",
1457
+ message: "Surviving mutant in " + entry.subject,
1458
+ why: "No test detects this code change, meaning this logic path is not properly verified.",
1459
+ suggestion: "Add a test that would fail if this mutation were applied.",
1460
+ metadata: { subject: entry.subject, source: "mutant" }
1461
+ };
1462
+ }
1463
+ function processEntries(opts) {
1464
+ const fileStatsMap = groupByFile2(opts.entries);
1465
+ const fileSet = new Set(opts.files);
1466
+ const findings = [];
1467
+ let totalKilled = 0;
1468
+ let totalMutants = 0;
1469
+ for (const [file, stats] of fileStatsMap) {
1470
+ if (!fileSet.has(file)) continue;
1471
+ totalKilled += stats.killed + stats.timeout;
1472
+ totalMutants += stats.total;
1473
+ if (stats.score < opts.threshold) {
1474
+ findings.push(fileStatsToFinding(file, stats, opts.threshold));
1475
+ findings.push(...stats.survivors.slice(0, opts.maxSurvivorFindings).map(survivorToFinding2));
1476
+ }
1477
+ }
1478
+ const mutationScore = opts.overallScore ?? (totalMutants > 0 ? totalKilled / totalMutants * 100 : 100);
1479
+ return { findings, mutationScore };
1480
+ }
1481
+ var MutantAdapter = class {
1482
+ name = "mutant";
1483
+ supportedLanguages = ["ruby"];
1484
+ async isAvailable() {
1485
+ return isBinaryAvailable("mutant");
1486
+ }
1487
+ async run(files, config) {
1488
+ if (files.length === 0) return { findings: [], mutationScore: 100 };
1489
+ const timeout = config.timeout ?? 3e5;
1490
+ const threshold = config.mutationScoreThreshold ?? 80;
1491
+ const maxSurvivorFindings = config.maxSurvivorFindings ?? 5;
1492
+ const stdout = await this.executeMutant(config.workdir, timeout);
1493
+ if (stdout === null) return timeoutResult("mutant", timeout);
1494
+ if (!stdout.trim()) return { findings: [], mutationScore: 100 };
1495
+ const { entries, overallScore } = parseOutput(stdout);
1496
+ if (entries.length === 0 && overallScore === null) return { findings: [], mutationScore: 100 };
1497
+ return processEntries({ entries, overallScore, files, threshold, maxSurvivorFindings });
1498
+ }
1499
+ async executeMutant(workdir, timeout) {
1500
+ const result = await execa13("mutant", ["run", "--include", "lib", "--use", "rspec"], {
1501
+ cwd: workdir,
1502
+ reject: false,
1503
+ timeout
1504
+ });
1505
+ if (result.timedOut) return null;
1506
+ return result.stdout || "";
1507
+ }
1508
+ };
1509
+
1510
+ // src/gates/test-quality.ts
1511
+ var TestQualityGate = class {
1512
+ name = "test_quality";
1513
+ stryker = new StrykerAdapter();
1514
+ mutmut = new MutmutAdapter();
1515
+ mutant = new MutantAdapter();
1516
+ async run(ctx) {
1517
+ const start = Date.now();
1518
+ const config = ctx.config.testQuality ?? DEFAULT_TEST_QUALITY;
1519
+ const available = await this.checkAvailability(config, ctx);
1520
+ if (!available.stryker && !available.mutmut && !available.mutant) {
1521
+ return toolMissingResult(this.name, start, "No mutation testing tools available", "Install: npm i -D @stryker-mutator/core | pip install mutmut | gem install mutant");
1522
+ }
1523
+ const { findings, mutationScore } = await this.collectFindings(available, config, ctx);
1524
+ const score = calculateTestQualityScore({ mutationScore });
1525
+ const status = deriveStatus(score, findings);
1526
+ return { gate: this.name, score, status, duration_ms: Date.now() - start, findings };
1527
+ }
1528
+ async checkAvailability(config, ctx) {
1529
+ const tsOrJs = ctx.language === "typescript" || ctx.language === "javascript";
1530
+ const isPython = ctx.language === "python";
1531
+ const isRuby = ctx.language === "ruby";
1532
+ return {
1533
+ stryker: config.strykerEnabled && tsOrJs ? await this.stryker.isAvailable(ctx.workdir) : false,
1534
+ mutmut: config.mutmutEnabled && isPython ? await this.mutmut.isAvailable() : false,
1535
+ mutant: config.mutantEnabled && isRuby ? await this.mutant.isAvailable() : false
1536
+ };
1537
+ }
1538
+ async collectFindings(available, config, ctx) {
1539
+ const allFindings = [];
1540
+ let totalScore = 0;
1541
+ let adapterCount = 0;
1542
+ const adapters = [
1543
+ { available: available.stryker, adapter: this.stryker },
1544
+ { available: available.mutmut, adapter: this.mutmut },
1545
+ { available: available.mutant, adapter: this.mutant }
1546
+ ];
1547
+ for (const { available: isAvail, adapter } of adapters) {
1548
+ if (!isAvail) continue;
1549
+ const result = await this.runAdapter(adapter, ctx, config);
1550
+ if (result) {
1551
+ allFindings.push(...result.findings);
1552
+ if (!result.timedOut) {
1553
+ totalScore += result.mutationScore;
1554
+ adapterCount++;
1555
+ }
1556
+ }
1557
+ }
1558
+ const mutationScore = adapterCount > 0 ? totalScore / adapterCount : 100;
1559
+ return { findings: allFindings, mutationScore };
1560
+ }
1561
+ async runAdapter(adapter, ctx, config) {
1562
+ const files = ctx.files.filter((f) => adapter.supportedLanguages.includes(f.language)).map((f) => f.relativePath);
1563
+ if (files.length === 0) return null;
1564
+ return adapter.run(files, {
1565
+ workdir: ctx.workdir,
1566
+ thresholds: {},
1567
+ timeout: config.timeout,
1568
+ mutationScoreThreshold: config.mutationScoreThreshold,
1569
+ maxSurvivorFindings: config.maxSurvivorFindings
1570
+ });
1571
+ }
1572
+ };
1573
+
1574
+ // src/gates/index.ts
1575
+ var GATE_REGISTRY = /* @__PURE__ */ new Map();
1576
+ function registerGate(gate) {
1577
+ GATE_REGISTRY.set(gate.name, gate);
1578
+ }
1579
+ function getGate(name) {
1580
+ return GATE_REGISTRY.get(name);
1581
+ }
1582
+ function getAllGateNames() {
1583
+ return Array.from(GATE_REGISTRY.keys());
1584
+ }
1585
+ registerGate(new TypeSafetyGate());
1586
+ registerGate(new ComplexityGate());
1587
+ registerGate(new SecurityGate());
1588
+ registerGate(new ArchitectureGate());
1589
+ registerGate(new TestQualityGate());
1590
+
1591
+ // src/runner.ts
1592
+ async function runGates(ctx, gateNames) {
1593
+ const results = [];
1594
+ for (const name of gateNames) {
1595
+ const gate = getGate(name);
1596
+ if (!gate) {
1597
+ results.push({
1598
+ gate: name,
1599
+ score: 0,
1600
+ status: "skip",
1601
+ duration_ms: 0,
1602
+ findings: [
1603
+ {
1604
+ file: "",
1605
+ line: 0,
1606
+ severity: "info",
1607
+ metric: "gate_not_found",
1608
+ message: `Gate "${name}" is not implemented yet.`,
1609
+ why: "This gate has not been registered."
1610
+ }
1611
+ ]
1612
+ });
1613
+ continue;
1614
+ }
1615
+ const result = await gate.run(ctx);
1616
+ results.push(result);
1617
+ if (result.status === "fail") break;
1618
+ }
1619
+ return results;
1620
+ }
1621
+
1622
+ // src/config/profiles.ts
1623
+ var PROFILES = {
1624
+ default: {
1625
+ name: "default",
1626
+ complexity: DEFAULT_COMPLEXITY,
1627
+ security: DEFAULT_SECURITY,
1628
+ typeSafety: DEFAULT_TYPE_SAFETY,
1629
+ architecture: DEFAULT_ARCHITECTURE,
1630
+ testQuality: DEFAULT_TEST_QUALITY
1631
+ },
1632
+ critical: {
1633
+ name: "critical",
1634
+ complexity: {
1635
+ cyclomatic: 6,
1636
+ length: 30,
1637
+ arguments: 3,
1638
+ nesting: 2
1639
+ },
1640
+ security: {
1641
+ semgrepRules: ["p/security-audit", "p/secrets", "p/owasp-top-ten"],
1642
+ gitleaksEnabled: true
1643
+ },
1644
+ typeSafety: {
1645
+ strict: true,
1646
+ mypyEnabled: true
1647
+ },
1648
+ architecture: {
1649
+ madgeEnabled: true,
1650
+ jscpdEnabled: true,
1651
+ knipEnabled: true,
1652
+ jscpdMinLines: 3,
1653
+ jscpdMinTokens: 30
1654
+ },
1655
+ testQuality: {
1656
+ strykerEnabled: true,
1657
+ mutmutEnabled: true,
1658
+ mutantEnabled: true,
1659
+ mutationScoreThreshold: 90,
1660
+ timeout: 6e5,
1661
+ maxSurvivorFindings: 10
1662
+ }
1663
+ },
1664
+ prototype: {
1665
+ name: "prototype",
1666
+ complexity: {
1667
+ cyclomatic: 15,
1668
+ length: 60,
1669
+ arguments: 6,
1670
+ nesting: 4
1671
+ },
1672
+ security: {
1673
+ semgrepRules: ["p/security-audit"],
1674
+ gitleaksEnabled: false
1675
+ },
1676
+ typeSafety: {
1677
+ strict: false,
1678
+ mypyEnabled: false
1679
+ },
1680
+ architecture: {
1681
+ madgeEnabled: false,
1682
+ jscpdEnabled: false,
1683
+ knipEnabled: false
1684
+ },
1685
+ testQuality: {
1686
+ strykerEnabled: false,
1687
+ mutmutEnabled: false,
1688
+ mutantEnabled: false,
1689
+ mutationScoreThreshold: 60,
1690
+ timeout: 12e4,
1691
+ maxSurvivorFindings: 3
1692
+ }
1693
+ }
1694
+ };
1695
+ function getProfile(name) {
1696
+ const profile = PROFILES[name];
1697
+ if (!profile) {
1698
+ throw new Error(`Unknown profile: ${name}. Available: ${Object.keys(PROFILES).join(", ")}`);
1699
+ }
1700
+ return profile;
1701
+ }
1702
+
1703
+ // src/reporter.ts
1704
+ import chalk from "chalk";
1705
+ function reportText(results, meta) {
1706
+ const lines = [];
1707
+ lines.push(chalk.bold("=== VALIDATOR REPORT ==="));
1708
+ lines.push(`Files analyzed: ${meta.filesAnalyzed} (mode: ${meta.mode})`);
1709
+ lines.push("");
1710
+ for (const result of results) {
1711
+ const icon = statusIcon(result.status);
1712
+ const score = `${result.score}%`.padStart(4);
1713
+ const duration = `(${(result.duration_ms / 1e3).toFixed(1)}s)`;
1714
+ lines.push(`${icon} ${result.gate.padEnd(20)} ${score} ${duration}`);
1715
+ }
1716
+ const allFindings = results.flatMap((r) => r.findings);
1717
+ if (allFindings.length > 0) {
1718
+ lines.push("");
1719
+ lines.push(chalk.bold("--- FINDINGS ---"));
1720
+ lines.push("");
1721
+ for (const f of allFindings) {
1722
+ const sev = f.severity === "blocker" ? chalk.red("[BLOCKER]") : f.severity === "warning" ? chalk.yellow("[WARNING]") : chalk.blue("[INFO]");
1723
+ const loc = f.function ? `${f.file}:${f.line} \u2014 ${f.function}` : `${f.file}:${f.line}`;
1724
+ lines.push(`${sev} ${loc}`);
1725
+ lines.push(` ${f.message}`);
1726
+ if (f.why) lines.push(` ${chalk.dim("Why:")} ${f.why}`);
1727
+ if (f.suggestion) lines.push(` ${chalk.dim("Fix:")} ${f.suggestion}`);
1728
+ lines.push("");
1729
+ }
1730
+ }
1731
+ const overall = overallStatus(results);
1732
+ const blockerCount = allFindings.filter((f) => f.severity === "blocker").length;
1733
+ const warnCount = allFindings.filter((f) => f.severity === "warning").length;
1734
+ lines.push(
1735
+ `RESULT: ${overall.toUpperCase()} (${blockerCount} blockers, ${warnCount} warnings)`
1736
+ );
1737
+ return lines.join("\n");
1738
+ }
1739
+ function reportJson(results, meta) {
1740
+ const allFindings = results.flatMap((r) => r.findings);
1741
+ const report = {
1742
+ version: "0.1.0",
1743
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1744
+ mode: meta.mode,
1745
+ files_analyzed: meta.filesAnalyzed,
1746
+ overall: {
1747
+ status: overallStatus(results),
1748
+ blocked_by: results.filter((r) => r.status === "fail").map((r) => r.gate),
1749
+ total_findings: allFindings.length,
1750
+ blockers: allFindings.filter((f) => f.severity === "blocker").length,
1751
+ warnings: allFindings.filter((f) => f.severity === "warning").length
1752
+ },
1753
+ gates: Object.fromEntries(results.map((r) => [r.gate, r]))
1754
+ };
1755
+ return JSON.stringify(report, null, 2);
1756
+ }
1757
+ function statusIcon(status) {
1758
+ switch (status) {
1759
+ case "pass":
1760
+ return chalk.green("[PASS]");
1761
+ case "fail":
1762
+ return chalk.red("[FAIL]");
1763
+ case "warn":
1764
+ return chalk.yellow("[WARN]");
1765
+ case "skip":
1766
+ return chalk.gray("[SKIP]");
1767
+ default:
1768
+ return "[????]";
1769
+ }
1770
+ }
1771
+
1772
+ // src/index.ts
1773
+ async function validate(options) {
1774
+ const workdir = options.workdir ?? process.cwd();
1775
+ const profile = getProfile(options.profile ?? "default");
1776
+ const gateNames = options.gates ?? getAllGateNames();
1777
+ const files = await resolveFiles({
1778
+ mode: options.mode,
1779
+ targets: options.targets,
1780
+ base: options.base,
1781
+ head: options.head,
1782
+ staged: options.staged,
1783
+ workdir
1784
+ });
1785
+ const language = detectPrimaryLanguage(files);
1786
+ const results = await runGates({ files, config: profile, workdir, language }, gateNames);
1787
+ const allFindings = results.flatMap((r) => r.findings);
1788
+ return {
1789
+ status: overallStatus(results),
1790
+ gates: results,
1791
+ filesAnalyzed: files.length,
1792
+ blockers: allFindings.filter((f) => f.severity === "blocker").length,
1793
+ warnings: allFindings.filter((f) => f.severity === "warning").length
1794
+ };
1795
+ }
1796
+ function formatReport(report, format = "text", mode = "scan") {
1797
+ const meta = { mode, filesAnalyzed: report.filesAnalyzed };
1798
+ return format === "json" ? reportJson(report.gates, meta) : reportText(report.gates, meta);
1799
+ }
1800
+ export {
1801
+ formatReport,
1802
+ validate
1803
+ };
1804
+ //# sourceMappingURL=index.js.map