@boyingliu01/xp-gate 0.9.5 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,5 @@
1
1
  import fs from 'fs/promises';
2
2
  import path from 'path';
3
- import { spawn } from 'child_process';
4
3
  import {
5
4
  GateMOptions,
6
5
  GateMResult,
@@ -8,16 +7,22 @@ import {
8
7
  MutationBaseline,
9
8
  MutationScore,
10
9
  FileThreshold,
11
- ScoreEvaluation,
12
- TestIntentCheckResult
10
+ ScoreEvaluation
13
11
  } from './types';
14
- import { StrykerReport } from './stryker-types';
15
12
  import { detectAITestCharacteristics } from './detect-ai-test';
13
+ import {
14
+ resolveRunner,
15
+ registerAllRunners,
16
+ RunMutationOptions,
17
+ MutationRunOutcome,
18
+ } from './runners';
19
+
20
+ // ── Pre-run auto-registration of all language runners ──
21
+
22
+ registerAllRunners();
16
23
 
17
24
  const DEFAULT_THRESHOLD = 60;
18
25
  const CRITICAL_PATH_THRESHOLD = 80;
19
- const STRYKER_REPORT_PATH = '.stryker-report.json';
20
- const STRYKER_CONFIG = 'stryker.prepush.conf.json';
21
26
 
22
27
  type ArgHandler = (options: GateMOptions, args: string[], i: number) => void;
23
28
 
@@ -25,7 +30,7 @@ const ARG_HANDLERS: Record<string, ArgHandler> = {
25
30
  '--changed-files': parseChangedFiles,
26
31
  '--baseline': parseBaseline,
27
32
  '--critical-paths': parseCriticalPaths,
28
- '--timeout': parseTimeout
33
+ '--timeout': parseTimeout,
29
34
  };
30
35
 
31
36
  function parseArgs(args: string[]): GateMOptions {
@@ -33,7 +38,7 @@ function parseArgs(args: string[]): GateMOptions {
33
38
  changedFiles: [],
34
39
  baselinePath: '.mutation-baseline.json',
35
40
  criticalPathsPath: '.mutation-critical-paths',
36
- timeoutMs: 120000
41
+ timeoutMs: 120000,
37
42
  };
38
43
 
39
44
  for (let i = 0; i < args.length; i++) {
@@ -68,13 +73,21 @@ function parseTimeout(options: GateMOptions, args: string[], i: number): void {
68
73
  if (next) options.timeoutMs = parseInt(next, 10);
69
74
  }
70
75
 
76
+ // ── File filtering: excludes tests, declarations, adapters ──
77
+ // Delegate per-language filtering to a helper.
78
+
71
79
  function filterSourceFiles(files: string[]): string[] {
72
80
  return files.filter(file => {
73
- if (!file.endsWith('.ts')) return false;
74
- if (file.endsWith('.test.ts')) return false;
81
+ const ext = path.extname(file);
82
+ // Skip test files
83
+ if (file.includes('.test.') || file.includes('_test.')) return false;
84
+ // Skip declaration files
75
85
  if (file.endsWith('.d.ts')) return false;
86
+ // Skip adapter files
76
87
  if (file.includes('/adapters/')) return false;
77
- return true;
88
+ // Skip files with no registered runner
89
+ const runner = resolveRunner(ext);
90
+ return runner !== undefined;
78
91
  });
79
92
  }
80
93
 
@@ -87,33 +100,51 @@ async function fileExists(filePath: string): Promise<boolean> {
87
100
  }
88
101
  }
89
102
 
90
- async function findTestFile(sourceFile: string): Promise<string | null> {
91
- const testFile1 = sourceFile.replace(/\.ts$/, '.test.ts');
92
- if (await fileExists(testFile1)) return testFile1;
93
-
103
+ async function findTestFileForSource(sourceFile: string): Promise<string | null> {
104
+ const ext = path.extname(sourceFile);
105
+ const baseName = path.basename(sourceFile, ext);
94
106
  const dir = path.dirname(sourceFile);
95
- const basename = path.basename(sourceFile, '.ts');
96
- const testFile2 = path.join(dir, '__tests__', `${basename}.test.ts`);
97
- if (await fileExists(testFile2)) return testFile2;
107
+
108
+ if (ext === '.ts' || ext === '.tsx') {
109
+ // TypeScript: foo.ts foo.test.ts
110
+ const testFile1 = sourceFile.replace(/\.tsx?$/, '.test.ts');
111
+ if (await fileExists(testFile1)) return testFile1;
112
+
113
+ const testFile2 = path.join(dir, '__tests__', `${baseName}.test.ts`);
114
+ if (await fileExists(testFile2)) return testFile2;
115
+ }
116
+
117
+ if (ext === '.py') {
118
+ // Python: foo.py → foo_test.py or tests/test_foo.py
119
+ const testFile1 = sourceFile.replace(/\.py$/, '_test.py');
120
+ if (await fileExists(testFile1)) return testFile1;
121
+
122
+ const testFile2 = path.join('tests', `test_${baseName}.py`);
123
+ if (await fileExists(testFile2)) return testFile2;
124
+
125
+ const testFile3 = path.join(dir, '__tests__', `test_${baseName}.py`);
126
+ if (await fileExists(testFile3)) return testFile3;
127
+ }
98
128
 
99
129
  return null;
100
130
  }
101
131
 
102
- async function checkTestIntents(
103
- sourceFiles: string[]
104
- ): Promise<TestIntentCheckResult[]> {
132
+ interface TestIntentCheckResult {
133
+ sourceFile: string;
134
+ testFile: string | null;
135
+ missingAnnotations: string[];
136
+ }
137
+
138
+ async function checkTestIntents(sourceFiles: string[]): Promise<TestIntentCheckResult[]> {
105
139
  const results: TestIntentCheckResult[] = [];
106
140
 
107
141
  for (const sourceFile of sourceFiles) {
108
- const testFile = await findTestFile(sourceFile);
142
+ const testFile = await findTestFileForSource(sourceFile);
109
143
  if (!testFile) {
110
144
  results.push({
111
145
  sourceFile,
112
146
  testFile: null,
113
- hasTestAnnotation: false,
114
- hasIntentAnnotation: false,
115
- hasCoversAnnotation: false,
116
- missingAnnotations: ['@test', '@intent', '@covers']
147
+ missingAnnotations: ['@test', '@intent', '@covers'],
117
148
  });
118
149
  continue;
119
150
  }
@@ -127,10 +158,7 @@ async function checkTestIntents(
127
158
  results.push({
128
159
  sourceFile,
129
160
  testFile,
130
- hasTestAnnotation: detection.annotations.hasTest,
131
- hasIntentAnnotation: detection.annotations.hasIntent,
132
- hasCoversAnnotation: detection.annotations.hasCovers,
133
- missingAnnotations
161
+ missingAnnotations,
134
162
  });
135
163
  }
136
164
 
@@ -204,7 +232,7 @@ async function determineThresholds(
204
232
  const thresholds: FileThreshold[] = [];
205
233
 
206
234
  for (const file of sourceFiles) {
207
- const testFile = await findTestFile(file);
235
+ const testFile = await findTestFileForSource(file);
208
236
  let explicitThreshold: number | undefined;
209
237
  if (testFile) {
210
238
  const detection = await detectAITestCharacteristics(testFile);
@@ -222,116 +250,33 @@ async function determineThresholds(
222
250
  file,
223
251
  threshold,
224
252
  isCriticalPath: isCritical,
225
- explicitThreshold
253
+ explicitThreshold,
226
254
  });
227
255
  }
228
256
 
229
257
  return thresholds;
230
258
  }
231
259
 
232
- function runStryker(
233
- files: string[],
234
- timeoutMs: number
235
- ): Promise<{ report: StrykerReport | null; timedOut: boolean; error?: string }> {
236
- return new Promise((resolve) => {
237
- const args = [
238
- 'stryker',
239
- 'run',
240
- '--config',
241
- STRYKER_CONFIG,
242
- ...files.flatMap(f => ['--mutate', f])
243
- ];
244
-
245
- const child = spawn('npx', args, {
246
- stdio: 'pipe',
247
- shell: false
248
- });
260
+ // ── Per-file extension grouping → route to correct runner ──
249
261
 
250
- let stdout = '';
251
- let stderr = '';
262
+ function groupByRunner(files: string[]): Map<string, string[]> {
263
+ const groups = new Map<string, string[]>();
252
264
 
253
- child.stdout?.on('data', (data: Buffer) => {
254
- stdout += data.toString();
255
- });
265
+ for (const file of files) {
266
+ const ext = path.extname(file);
267
+ const runner = resolveRunner(ext);
268
+ if (!runner) continue;
256
269
 
257
- child.stderr?.on('data', (data: Buffer) => {
258
- stderr += data.toString();
259
- });
260
-
261
- const timeoutId = setTimeout(() => {
262
- child.kill('SIGTERM');
263
- setTimeout(() => {
264
- if (!child.killed) {
265
- child.kill('SIGKILL');
266
- }
267
- }, 5000);
268
- }, timeoutMs);
269
-
270
- child.on('close', async (code) => {
271
- clearTimeout(timeoutId);
272
-
273
- if (code === null) {
274
- resolve({ report: null, timedOut: true });
275
- return;
276
- }
277
-
278
- const report = await parseStrykerReport(STRYKER_REPORT_PATH);
279
- resolve({ report, timedOut: false, error: code !== 0 ? stderr || stdout : undefined });
280
- });
281
-
282
- child.on('error', (err) => {
283
- clearTimeout(timeoutId);
284
- resolve({ report: null, timedOut: false, error: err.message });
285
- });
286
- });
287
- }
288
-
289
- async function parseStrykerReport(reportPath: string): Promise<StrykerReport | null> {
290
- try {
291
- const content = await fs.readFile(reportPath, 'utf-8');
292
- const parsed = JSON.parse(content);
293
- return parseReportObject(parsed);
294
- } catch {
295
- return null;
270
+ const key = runner.name;
271
+ if (!groups.has(key)) groups.set(key, []);
272
+ groups.get(key)!.push(file);
296
273
  }
297
- }
298
-
299
- function parseReportObject(parsed: Record<string, unknown>): StrykerReport {
300
- const report: StrykerReport = {
301
- mutationScore: asNumber(parsed.mutationScore),
302
- nrOfMutants: asNumber(parsed.nrOfMutants),
303
- nrOfKilledMutants: asNumber(parsed.nrOfKilledMutants),
304
- nrOfSurvivedMutants: asNumber(parsed.nrOfSurvivedMutants),
305
- };
306
274
 
307
- if (parsed.files && typeof parsed.files === 'object') {
308
- report.files = parseFilesObject(parsed.files as Record<string, Record<string, unknown>>);
309
- }
310
-
311
- return report;
312
- }
313
-
314
- function asNumber(value: unknown, fallback = 0): number {
315
- return typeof value === 'number' ? value : fallback;
316
- }
317
-
318
- function parseFilesObject(
319
- filesObj: Record<string, Record<string, unknown>>
320
- ): Record<string, { mutationScore: number; nrOfMutants: number; nrOfKilledMutants: number; nrOfSurvivedMutants: number }> {
321
- const files: Record<string, { mutationScore: number; nrOfMutants: number; nrOfKilledMutants: number; nrOfSurvivedMutants: number }> = {};
322
- for (const [file, data] of Object.entries(filesObj)) {
323
- files[file] = {
324
- mutationScore: asNumber(data.mutationScore),
325
- nrOfMutants: asNumber(data.nrOfMutants),
326
- nrOfKilledMutants: asNumber(data.nrOfKilledMutants),
327
- nrOfSurvivedMutants: asNumber(data.nrOfSurvivedMutants),
328
- };
329
- }
330
- return files;
275
+ return groups;
331
276
  }
332
277
 
333
278
  function evaluateScores(
334
- report: StrykerReport,
279
+ fileScores: Record<string, number>,
335
280
  thresholds: FileThreshold[],
336
281
  baseline: MutationBaseline | null
337
282
  ): { evaluations: ScoreEvaluation[]; blocked: boolean; messages: string[] } {
@@ -340,47 +285,39 @@ function evaluateScores(
340
285
  let blocked = false;
341
286
 
342
287
  for (const ft of thresholds) {
343
- const result = evaluateFileThreshold(ft, report, baseline);
344
- evaluations.push(result.evaluation);
345
- if (!result.evaluation.passed) blocked = true;
346
- messages.push(result.message);
347
- }
348
-
349
- return { evaluations, blocked, messages };
350
- }
288
+ const score = fileScores[ft.file] ?? 0;
289
+ const baselineScore = baseline?.scores[ft.file]?.score;
351
290
 
352
- function evaluateFileThreshold(
353
- ft: FileThreshold,
354
- report: StrykerReport,
355
- baseline: MutationBaseline | null
356
- ): { evaluation: ScoreEvaluation; message: string } {
357
- const fileReport = report.files
358
- ? Object.entries(report.files).find(([key]) => key === ft.file)?.[1]
359
- : undefined;
291
+ let effectiveThreshold = ft.threshold;
292
+ if (baselineScore !== undefined && score < baselineScore) {
293
+ effectiveThreshold = Math.max(effectiveThreshold, baselineScore);
294
+ }
360
295
 
361
- const score = fileReport?.mutationScore ?? report.mutationScore;
362
- const baselineScore = baseline?.scores[ft.file]?.score;
296
+ const passed = score >= effectiveThreshold;
297
+ const isRegression = baselineScore !== undefined && score < baselineScore;
363
298
 
364
- let effectiveThreshold = ft.threshold;
365
- if (baselineScore !== undefined && score < baselineScore) {
366
- effectiveThreshold = Math.max(effectiveThreshold, baselineScore);
299
+ const evaluation: ScoreEvaluation = {
300
+ file: ft.file,
301
+ score,
302
+ threshold: effectiveThreshold,
303
+ baselineScore,
304
+ passed,
305
+ isRegression,
306
+ };
307
+ evaluations.push(evaluation);
308
+
309
+ if (!passed) blocked = true;
310
+ messages.push(buildScoreMessage({
311
+ file: ft.file,
312
+ score,
313
+ effectiveThreshold,
314
+ baselineScore,
315
+ passed,
316
+ isRegression,
317
+ }));
367
318
  }
368
319
 
369
- const passed = score >= effectiveThreshold;
370
- const isRegression = baselineScore !== undefined && score < baselineScore;
371
-
372
- const evaluation: ScoreEvaluation = {
373
- file: ft.file,
374
- score,
375
- threshold: effectiveThreshold,
376
- baselineScore,
377
- passed,
378
- isRegression
379
- };
380
-
381
- const message = buildScoreMessage({ file: ft.file, score, effectiveThreshold, baselineScore, passed, isRegression });
382
-
383
- return { evaluation, message };
320
+ return { evaluations, blocked, messages };
384
321
  }
385
322
 
386
323
  interface ScoreMessageParams {
@@ -416,63 +353,86 @@ function buildResult(
416
353
  filesChecked,
417
354
  scores,
418
355
  warnings,
419
- errors
356
+ errors,
420
357
  };
421
358
  }
422
359
 
423
- export async function runGateM(options: GateMOptions): Promise<GateMResult> {
424
- const warnings: string[] = [];
425
- const errors: string[] = [];
426
-
427
- const sourceFiles = filterSourceFiles(options.changedFiles);
360
+ async function runAllRunners(
361
+ groups: Map<string, string[]>,
362
+ timeoutMs: number,
363
+ cwd: string
364
+ ): Promise<Record<string, number | null>> {
365
+ const fileScores: Record<string, number | null> = {};
428
366
 
429
- if (sourceFiles.length === 0) {
430
- return buildResult('skip', 0, {}, warnings, errors);
431
- }
367
+ for (const [, files] of Array.from(groups.entries())) {
368
+ const runner = resolveRunner(path.extname(files[0]));
369
+ if (!runner) continue;
432
370
 
433
- collectTestIntentWarnings(sourceFiles, warnings);
371
+ if (!await runner.isAvailable()) {
372
+ for (const f of files) fileScores[f] = null;
373
+ continue;
374
+ }
434
375
 
435
- const thresholds = await determineThresholdsWithLogging(options, sourceFiles);
436
- const baseline = await loadBaselineWithLogging(options.baselinePath);
376
+ const options: RunMutationOptions = {
377
+ files,
378
+ timeoutMs,
379
+ cwd,
380
+ };
437
381
 
438
- const strykerResult = await runStryker(sourceFiles, options.timeoutMs);
439
- const timeoutResult = handleTimeout(strykerResult, options, sourceFiles, warnings, errors);
440
- if (timeoutResult) return timeoutResult;
382
+ const outcome: MutationRunOutcome = await runner.run(options);
441
383
 
442
- const errorResult = handleStrykerError(strykerResult, sourceFiles, warnings, errors);
443
- if (errorResult) return errorResult;
384
+ if (outcome.timedOut) {
385
+ for (const f of files) fileScores[f] = null;
386
+ continue;
387
+ }
444
388
 
445
- const reportResult = handleMissingReport(strykerResult, sourceFiles, warnings, errors);
446
- if (reportResult) return reportResult;
389
+ if (outcome.error) {
390
+ for (const f of files) fileScores[f] = null;
391
+ continue;
392
+ }
447
393
 
448
- const evalResult = evaluateAndReport(strykerResult.report, thresholds, baseline);
394
+ if (!outcome.report) {
395
+ for (const f of files) fileScores[f] = null;
396
+ continue;
397
+ }
449
398
 
450
- if (evalResult.blocked) {
451
- return buildResult('block', sourceFiles.length, evalResult.scores, warnings, errors);
399
+ // If per-file scores exist, use them
400
+ if (outcome.report.files) {
401
+ for (const [fileKey, fileReport] of Object.entries(outcome.report.files)) {
402
+ fileScores[fileKey] = fileReport.mutationScore;
403
+ }
404
+ } else {
405
+ // Top-level score applies to all files
406
+ for (const f of files) {
407
+ fileScores[f] = outcome.report.mutationScore;
408
+ }
409
+ }
452
410
  }
453
411
 
454
- return buildResult('pass', sourceFiles.length, evalResult.scores, warnings, errors);
412
+ return fileScores;
455
413
  }
456
414
 
457
- function collectTestIntentWarnings(sourceFiles: string[], warnings: string[]): void {
458
- checkTestIntents(sourceFiles).then((results) => {
459
- for (const result of results) {
460
- if (result.missingAnnotations.length > 0) {
461
- const testFileInfo = result.testFile ? ` (${result.testFile})` : ' (no test file found)';
462
- warnings.push(
463
- `Warning: ${result.sourceFile}${testFileInfo} missing annotations: ${result.missingAnnotations.join(', ')}`
464
- );
465
- }
466
- }
467
- });
468
- }
415
+ // ── Main gateway ──
469
416
 
470
- async function determineThresholdsWithLogging(
471
- options: GateMOptions,
472
- sourceFiles: string[]
473
- ): Promise<FileThreshold[]> {
474
- const criticalPaths = await loadCriticalPaths(options.criticalPathsPath);
475
- const thresholds = await determineThresholds(sourceFiles, criticalPaths);
417
+ export async function runGateM(options: GateMOptions): Promise<GateMResult> {
418
+ const warnings: string[] = [];
419
+ const errors: string[] = [];
420
+
421
+ const sourceFiles = filterSourceFiles(options.changedFiles);
422
+
423
+ if (sourceFiles.length === 0) {
424
+ return buildResult('skip', 0, {}, warnings, errors);
425
+ }
426
+
427
+ // Test intent check (async, fire-and-forget warning)
428
+ await collectTestIntentWarnings(sourceFiles, warnings);
429
+
430
+ const thresholds = await determineThresholds(sourceFiles, await loadCriticalPaths(options.criticalPathsPath));
431
+ const baseline = await loadBaseline(options.baselinePath);
432
+
433
+ if (baseline) {
434
+ console.log(` Loaded baseline from ${options.baselinePath} (${Object.keys(baseline.scores).length} files)`);
435
+ }
476
436
 
477
437
  for (const ft of thresholds) {
478
438
  const level = ft.isCriticalPath ? 'critical path' : 'default';
@@ -484,84 +444,60 @@ async function determineThresholdsWithLogging(
484
444
  console.log(` ${ft.file}: threshold=${ft.threshold}% (${level}, ${thresholdSource})`);
485
445
  }
486
446
 
487
- return thresholds;
488
- }
447
+ // Route to runners by file extension
448
+ const groups = groupByRunner(sourceFiles);
449
+ const cwd = process.cwd();
450
+ const fileScoresNullable = await runAllRunners(groups, options.timeoutMs, cwd);
489
451
 
490
- async function loadBaselineWithLogging(baselinePath: string): Promise<MutationBaseline | null> {
491
- const baseline = await loadBaseline(baselinePath);
492
- if (baseline) {
493
- console.log(` Loaded baseline from ${baselinePath} (${Object.keys(baseline.scores).length} files)`);
452
+ // Build final scores map
453
+ const fileScores: Record<string, number> = {};
454
+ const timeoutFiles: string[] = [];
455
+
456
+ for (const [file, maybeScore] of Object.entries(fileScoresNullable)) {
457
+ if (maybeScore === null) {
458
+ timeoutFiles.push(file);
459
+ } else {
460
+ fileScores[file] = maybeScore;
461
+ }
494
462
  }
495
- return baseline;
496
- }
497
463
 
498
- function handleTimeout(
499
- strykerResult: { report: StrykerReport | null; timedOut: boolean; error?: string },
500
- options: GateMOptions,
501
- sourceFiles: string[],
502
- warnings: string[],
503
- errors: string[]
504
- ): GateMResult | null {
505
- if (!strykerResult.timedOut) return null;
464
+ if (timeoutFiles.length > 0) {
465
+ warnings.push(`Mutation testing timed out for: ${timeoutFiles.join(', ')}. Run locally for full report.`);
466
+ }
506
467
 
507
- warnings.push(
508
- `Mutation testing timed out (>${options.timeoutMs}ms). Push allowed. Run 'npm run test:mutation' locally for full report.`
509
- );
510
- return buildResult('timeout', sourceFiles.length, {}, warnings, errors);
511
- }
468
+ const evalResult = evaluateScores(fileScores, thresholds, baseline);
512
469
 
513
- function handleStrykerError(
514
- strykerResult: { report: StrykerReport | null; timedOut: boolean; error?: string },
515
- sourceFiles: string[],
516
- warnings: string[],
517
- errors: string[]
518
- ): GateMResult | null {
519
- if (!strykerResult.error || strykerResult.report) return null;
470
+ for (const msg of evalResult.messages) {
471
+ console.log(` ${msg}`);
472
+ }
520
473
 
521
- errors.push(`Stryker failed: ${strykerResult.error}`);
522
- return buildResult('block', sourceFiles.length, {}, warnings, errors);
523
- }
474
+ const mutationScores: Record<string, MutationScore> = {};
475
+ for (const [file, score] of Object.entries(fileScores)) {
476
+ mutationScores[file] = {
477
+ score,
478
+ mutants: 0, // Detailed stats only available from per-file reports
479
+ killed: 0,
480
+ survived: 0,
481
+ };
482
+ }
524
483
 
525
- function handleMissingReport(
526
- strykerResult: { report: StrykerReport | null; timedOut: boolean; error?: string },
527
- sourceFiles: string[],
528
- warnings: string[],
529
- errors: string[]
530
- ): GateMResult | null {
531
- if (strykerResult.report) return null;
484
+ if (evalResult.blocked) {
485
+ return buildResult('block', sourceFiles.length, mutationScores, warnings, errors);
486
+ }
532
487
 
533
- errors.push('Stryker report not found or invalid.');
534
- return buildResult('block', sourceFiles.length, {}, warnings, errors);
488
+ return buildResult('pass', sourceFiles.length, mutationScores, warnings, errors);
535
489
  }
536
490
 
537
- function evaluateAndReport(
538
- report: StrykerReport | null,
539
- thresholds: FileThreshold[],
540
- baseline: MutationBaseline | null
541
- ): { scores: Record<string, MutationScore>; blocked: boolean; messages: string[] } {
542
- if (!report) {
543
- return { scores: {}, blocked: true, messages: ['Stryker report unavailable'] };
544
- }
545
- const scores: Record<string, MutationScore> = {};
546
-
547
- if (report.files) {
548
- for (const [file, data] of Object.entries(report.files)) {
549
- scores[file] = {
550
- score: data.mutationScore,
551
- mutants: data.nrOfMutants,
552
- killed: data.nrOfKilledMutants,
553
- survived: data.nrOfSurvivedMutants
554
- };
491
+ async function collectTestIntentWarnings(sourceFiles: string[], warnings: string[]): Promise<void> {
492
+ const results = await checkTestIntents(sourceFiles);
493
+ for (const result of results) {
494
+ if (result.missingAnnotations.length > 0) {
495
+ const testFileInfo = result.testFile ? ` (${result.testFile})` : ' (no test file found)';
496
+ warnings.push(
497
+ `Warning: ${result.sourceFile}${testFileInfo} missing annotations: ${result.missingAnnotations.join(', ')}`
498
+ );
555
499
  }
556
500
  }
557
-
558
- const evalResult = evaluateScores(report, thresholds, baseline);
559
-
560
- for (const msg of evalResult.messages) {
561
- console.log(` ${msg}`);
562
- }
563
-
564
- return { scores, blocked: evalResult.blocked, messages: evalResult.messages.filter(Boolean) };
565
501
  }
566
502
 
567
503
  export async function main(args: string[]): Promise<number> {
@@ -572,7 +508,7 @@ export async function main(args: string[]): Promise<number> {
572
508
  return 1;
573
509
  }
574
510
 
575
- console.log(`Gate M: Mutation Testing`);
511
+ console.log(`Gate M: Mutation Testing (LangAdapter)`);
576
512
  console.log(` Changed files: ${options.changedFiles.length}`);
577
513
  console.log(` Baseline: ${options.baselinePath}`);
578
514
  console.log(` Critical paths: ${options.criticalPathsPath}`);
@@ -603,9 +539,9 @@ export async function main(args: string[]): Promise<number> {
603
539
  return result.exitCode;
604
540
  }
605
541
 
606
- const args = process.argv.slice(2);
542
+ const cliArgs = process.argv.slice(2);
607
543
  if (typeof require !== 'undefined' && require.main === module) {
608
- main(args)
544
+ main(cliArgs)
609
545
  .then(exitCode => {
610
546
  if (exitCode !== 0) {
611
547
  process.exit(exitCode);
@@ -616,4 +552,3 @@ if (typeof require !== 'undefined' && require.main === module) {
616
552
  process.exit(1);
617
553
  });
618
554
  }
619
- // trigger Gate M
@@ -0,0 +1,22 @@
1
+ export type {
2
+ MutationFileReport,
3
+ MutationRunResult,
4
+ MutationRunOutcome,
5
+ RunMutationOptions,
6
+ MutationRunner,
7
+ } from './types';
8
+
9
+ export { registerRunner, resolveRunner, runnerRegistry } from './types';
10
+
11
+ export { StrykerRunner } from './stryker-runner';
12
+ export { MutmutRunner } from './mutmut-runner';
13
+
14
+ import { StrykerRunner } from './stryker-runner';
15
+ import { MutmutRunner } from './mutmut-runner';
16
+ import { registerRunner } from './types';
17
+
18
+ /** Auto-register all known runners. */
19
+ export function registerAllRunners(): void {
20
+ registerRunner(new StrykerRunner());
21
+ registerRunner(new MutmutRunner());
22
+ }