@boyingliu01/xp-gate 0.11.3 → 0.11.4
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/adapter-common.sh +135 -55
- package/hooks/adapter-common.sh +135 -55
- package/mock-policy/AGENTS.md +4 -4
- package/mutation/AGENTS.md +4 -4
- package/mutation/__tests__/pitest-runner.test.ts +574 -0
- package/mutation/gate-m.ts +138 -94
- package/mutation/runners/index.ts +3 -0
- package/mutation/runners/pitest-runner.ts +290 -0
- package/package.json +1 -1
- package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
- package/plugins/claude-code/skills/delphi-review/AGENTS.md +4 -4
- package/plugins/claude-code/skills/sprint-flow/AGENTS.md +4 -4
- package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +4 -4
- package/plugins/opencode/package.json +1 -1
- package/plugins/opencode/skills/delphi-review/AGENTS.md +4 -4
- package/plugins/opencode/skills/sprint-flow/AGENTS.md +4 -4
- package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +4 -4
- package/plugins/qoder/plugin.json +1 -1
- package/plugins/qoder/skills/delphi-review/AGENTS.md +4 -4
- package/plugins/qoder/skills/sprint-flow/AGENTS.md +4 -4
- package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +4 -4
- package/principles/AGENTS.md +4 -4
- package/skills/delphi-review/AGENTS.md +4 -4
- package/skills/sprint-flow/AGENTS.md +4 -4
- package/skills/test-specification-alignment/AGENTS.md +4 -4
package/mutation/gate-m.ts
CHANGED
|
@@ -13,7 +13,6 @@ import { detectAITestCharacteristics } from './detect-ai-test';
|
|
|
13
13
|
import {
|
|
14
14
|
resolveRunner,
|
|
15
15
|
registerAllRunners,
|
|
16
|
-
RunMutationOptions,
|
|
17
16
|
MutationRunOutcome,
|
|
18
17
|
} from './runners';
|
|
19
18
|
|
|
@@ -81,6 +80,10 @@ function filterSourceFiles(files: string[]): string[] {
|
|
|
81
80
|
const ext = path.extname(file);
|
|
82
81
|
// Skip test files
|
|
83
82
|
if (file.includes('.test.') || file.includes('_test.')) return false;
|
|
83
|
+
// Skip Java/Kotlin test files: FooTest.java, FooSpec.kt, etc.
|
|
84
|
+
if (/[A-Z]Test\.(java|kt|kts)$/.test(file)) return false;
|
|
85
|
+
if (/[A-Z]Tests\.(java|kt|kts)$/.test(file)) return false;
|
|
86
|
+
if (/[A-Z](IT|Spec)\.(java|kt|kts)$/.test(file)) return false;
|
|
84
87
|
// Skip declaration files
|
|
85
88
|
if (file.endsWith('.d.ts')) return false;
|
|
86
89
|
// Skip adapter files
|
|
@@ -100,36 +103,85 @@ async function fileExists(filePath: string): Promise<boolean> {
|
|
|
100
103
|
}
|
|
101
104
|
}
|
|
102
105
|
|
|
106
|
+
async function findFirstExist(candidates: string[]): Promise<string | null> {
|
|
107
|
+
for (const c of candidates) {
|
|
108
|
+
if (await fileExists(c)) return c;
|
|
109
|
+
}
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function findTsTestFile(sourceFile: string, dir: string, baseName: string): Promise<string | null> {
|
|
114
|
+
return findFirstExist([
|
|
115
|
+
sourceFile.replace(/\.tsx?$/, '.test.ts'),
|
|
116
|
+
path.join(dir, '__tests__', `${baseName}.test.ts`),
|
|
117
|
+
]);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function findPyTestFile(dir: string, baseName: string): Promise<string | null> {
|
|
121
|
+
return findFirstExist([
|
|
122
|
+
'', // placeholder for the side-by-side _test.py case (handled inline below)
|
|
123
|
+
path.join('tests', `test_${baseName}.py`),
|
|
124
|
+
path.join(dir, '__tests__', `test_${baseName}.py`),
|
|
125
|
+
]);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function findJavaTestFile(dir: string, baseName: string): Promise<string | null> {
|
|
129
|
+
const dirs = [
|
|
130
|
+
dir.replace(/\/src\/main\//, '/src/test/'),
|
|
131
|
+
dir.replace(/^src\//, 'src/test/'),
|
|
132
|
+
];
|
|
133
|
+
const suffixes = ['Test.java', 'Tests.java', 'IT.java'];
|
|
134
|
+
for (const d of dirs) {
|
|
135
|
+
for (const s of suffixes) {
|
|
136
|
+
const c = path.join(d, `${baseName}${s}`);
|
|
137
|
+
if (await fileExists(c)) return c;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function findKotlinTestFile(dir: string, baseName: string): Promise<string | null> {
|
|
144
|
+
const dirs = [
|
|
145
|
+
dir.replace(/\/src\/main\//, '/src/test/'),
|
|
146
|
+
dir.replace(/^src\//, 'src/test/'),
|
|
147
|
+
];
|
|
148
|
+
const suffixes = ['Test.kt', 'Tests.kt', 'Spec.kt'];
|
|
149
|
+
for (const d of dirs) {
|
|
150
|
+
for (const s of suffixes) {
|
|
151
|
+
const c = path.join(d, `${baseName}${s}`);
|
|
152
|
+
if (await fileExists(c)) return c;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
|
|
103
158
|
async function findTestFileForSource(sourceFile: string): Promise<string | null> {
|
|
104
159
|
const ext = path.extname(sourceFile);
|
|
105
160
|
const baseName = path.basename(sourceFile, ext);
|
|
106
161
|
const dir = path.dirname(sourceFile);
|
|
107
162
|
|
|
108
163
|
if (ext === '.ts' || ext === '.tsx') {
|
|
109
|
-
|
|
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;
|
|
164
|
+
return findTsTestFile(sourceFile, dir, baseName);
|
|
115
165
|
}
|
|
116
166
|
|
|
117
167
|
if (ext === '.py') {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
168
|
+
const pyTest = sourceFile.replace(/\.py$/, '_test.py');
|
|
169
|
+
if (await fileExists(pyTest)) return pyTest;
|
|
170
|
+
return findPyTestFile(dir, baseName);
|
|
171
|
+
}
|
|
121
172
|
|
|
122
|
-
|
|
123
|
-
|
|
173
|
+
if (ext === '.go') {
|
|
174
|
+
const goTest = sourceFile.replace(/\.go$/, '_test.go');
|
|
175
|
+
if (await fileExists(goTest)) return goTest;
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
124
178
|
|
|
125
|
-
|
|
126
|
-
|
|
179
|
+
if (ext === '.java') {
|
|
180
|
+
return findJavaTestFile(dir, baseName);
|
|
127
181
|
}
|
|
128
182
|
|
|
129
|
-
if (ext === '.
|
|
130
|
-
|
|
131
|
-
const testFile1 = sourceFile.replace(/\.go$/, '_test.go');
|
|
132
|
-
if (await fileExists(testFile1)) return testFile1;
|
|
183
|
+
if (ext === '.kt' || ext === '.kts') {
|
|
184
|
+
return findKotlinTestFile(dir, baseName);
|
|
133
185
|
}
|
|
134
186
|
|
|
135
187
|
return null;
|
|
@@ -375,123 +427,115 @@ async function runAllRunners(
|
|
|
375
427
|
if (!runner) continue;
|
|
376
428
|
|
|
377
429
|
if (!await runner.isAvailable()) {
|
|
378
|
-
|
|
430
|
+
applyNullScores(files, fileScores);
|
|
379
431
|
continue;
|
|
380
432
|
}
|
|
381
433
|
|
|
382
|
-
const
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
cwd,
|
|
386
|
-
};
|
|
387
|
-
|
|
388
|
-
const outcome: MutationRunOutcome = await runner.run(options);
|
|
434
|
+
const result = await runner.run({ files, timeoutMs, cwd });
|
|
435
|
+
applyMutationResult(result, files, fileScores);
|
|
436
|
+
}
|
|
389
437
|
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
continue;
|
|
393
|
-
}
|
|
438
|
+
return fileScores;
|
|
439
|
+
}
|
|
394
440
|
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
}
|
|
441
|
+
function applyNullScores(files: string[], scores: Record<string, number | null>): void {
|
|
442
|
+
for (const f of files) scores[f] = null;
|
|
443
|
+
}
|
|
399
444
|
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
445
|
+
function applyMutationResult(
|
|
446
|
+
outcome: MutationRunOutcome,
|
|
447
|
+
files: string[],
|
|
448
|
+
scores: Record<string, number | null>,
|
|
449
|
+
): void {
|
|
450
|
+
if (outcome.timedOut || outcome.error || !outcome.report) {
|
|
451
|
+
for (const f of files) scores[f] = null;
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
404
454
|
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
fileScores[fileKey] = fileReport.mutationScore;
|
|
409
|
-
}
|
|
410
|
-
} else {
|
|
411
|
-
// Top-level score applies to all files
|
|
412
|
-
for (const f of files) {
|
|
413
|
-
fileScores[f] = outcome.report.mutationScore;
|
|
414
|
-
}
|
|
455
|
+
if (outcome.report.files) {
|
|
456
|
+
for (const [fileKey, fileReport] of Object.entries(outcome.report.files)) {
|
|
457
|
+
scores[fileKey] = fileReport.mutationScore;
|
|
415
458
|
}
|
|
459
|
+
return;
|
|
416
460
|
}
|
|
417
461
|
|
|
418
|
-
|
|
462
|
+
for (const f of files) scores[f] = outcome.report.mutationScore;
|
|
419
463
|
}
|
|
420
464
|
|
|
421
465
|
// ── Main gateway ──
|
|
422
466
|
|
|
467
|
+
function logThresholdDetails(thresholds: FileThreshold[], baseline: MutationBaseline | null): void {
|
|
468
|
+
if (baseline) {
|
|
469
|
+
console.log(` Loaded baseline (${Object.keys(baseline.scores).length} files)`);
|
|
470
|
+
}
|
|
471
|
+
for (const ft of thresholds) {
|
|
472
|
+
const level = ft.isCriticalPath ? 'critical path' : 'default';
|
|
473
|
+
const thresholdSource = ft.explicitThreshold !== undefined
|
|
474
|
+
? 'explicit annotation'
|
|
475
|
+
: ft.isCriticalPath
|
|
476
|
+
? 'critical path config'
|
|
477
|
+
: 'default';
|
|
478
|
+
console.log(` ${ft.file}: threshold=${ft.threshold}% (${level}, ${thresholdSource})`);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function buildMutationScores(fileScores: Record<string, number>): Record<string, MutationScore> {
|
|
483
|
+
const scores: Record<string, MutationScore> = {};
|
|
484
|
+
for (const [file, score] of Object.entries(fileScores)) {
|
|
485
|
+
scores[file] = { score, mutants: 0, killed: 0, survived: 0 };
|
|
486
|
+
}
|
|
487
|
+
return scores;
|
|
488
|
+
}
|
|
489
|
+
|
|
423
490
|
export async function runGateM(options: GateMOptions): Promise<GateMResult> {
|
|
424
491
|
const warnings: string[] = [];
|
|
425
492
|
const errors: string[] = [];
|
|
426
493
|
|
|
427
494
|
const sourceFiles = filterSourceFiles(options.changedFiles);
|
|
428
|
-
|
|
429
495
|
if (sourceFiles.length === 0) {
|
|
430
496
|
return buildResult('skip', 0, {}, warnings, errors);
|
|
431
497
|
}
|
|
432
498
|
|
|
433
|
-
// Test intent check (async, fire-and-forget warning)
|
|
434
499
|
await collectTestIntentWarnings(sourceFiles, warnings);
|
|
435
500
|
|
|
436
501
|
const thresholds = await determineThresholds(sourceFiles, await loadCriticalPaths(options.criticalPathsPath));
|
|
437
502
|
const baseline = await loadBaseline(options.baselinePath);
|
|
438
503
|
|
|
439
|
-
|
|
440
|
-
console.log(` Loaded baseline from ${options.baselinePath} (${Object.keys(baseline.scores).length} files)`);
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
for (const ft of thresholds) {
|
|
444
|
-
const level = ft.isCriticalPath ? 'critical path' : 'default';
|
|
445
|
-
const thresholdSource = ft.explicitThreshold !== undefined
|
|
446
|
-
? 'explicit annotation'
|
|
447
|
-
: ft.isCriticalPath
|
|
448
|
-
? 'critical path config'
|
|
449
|
-
: 'default';
|
|
450
|
-
console.log(` ${ft.file}: threshold=${ft.threshold}% (${level}, ${thresholdSource})`);
|
|
451
|
-
}
|
|
504
|
+
logThresholdDetails(thresholds, baseline);
|
|
452
505
|
|
|
453
|
-
// Route to runners by file extension
|
|
454
506
|
const groups = groupByRunner(sourceFiles);
|
|
455
|
-
const
|
|
456
|
-
const fileScoresNullable = await runAllRunners(groups, options.timeoutMs, cwd);
|
|
457
|
-
|
|
458
|
-
// Build final scores map
|
|
459
|
-
const fileScores: Record<string, number> = {};
|
|
460
|
-
const timeoutFiles: string[] = [];
|
|
461
|
-
|
|
462
|
-
for (const [file, maybeScore] of Object.entries(fileScoresNullable)) {
|
|
463
|
-
if (maybeScore === null) {
|
|
464
|
-
timeoutFiles.push(file);
|
|
465
|
-
} else {
|
|
466
|
-
fileScores[file] = maybeScore;
|
|
467
|
-
}
|
|
468
|
-
}
|
|
507
|
+
const fileScoresNullable = await runAllRunners(groups, options.timeoutMs, process.cwd());
|
|
469
508
|
|
|
509
|
+
const { fileScores, timeoutFiles } = partitionScores(fileScoresNullable);
|
|
470
510
|
if (timeoutFiles.length > 0) {
|
|
471
511
|
warnings.push(`Mutation testing timed out for: ${timeoutFiles.join(', ')}. Run locally for full report.`);
|
|
472
512
|
}
|
|
473
513
|
|
|
474
514
|
const evalResult = evaluateScores(fileScores, thresholds, baseline);
|
|
515
|
+
for (const msg of evalResult.messages) console.log(` ${msg}`);
|
|
475
516
|
|
|
476
|
-
|
|
477
|
-
|
|
517
|
+
if (evalResult.blocked) {
|
|
518
|
+
return buildResult('block', sourceFiles.length, buildMutationScores(fileScores), warnings, errors);
|
|
478
519
|
}
|
|
520
|
+
return buildResult('pass', sourceFiles.length, buildMutationScores(fileScores), warnings, errors);
|
|
521
|
+
}
|
|
479
522
|
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
mutants: 0, // Detailed stats only available from per-file reports
|
|
485
|
-
killed: 0,
|
|
486
|
-
survived: 0,
|
|
487
|
-
};
|
|
488
|
-
}
|
|
523
|
+
interface PartitionedScores {
|
|
524
|
+
fileScores: Record<string, number>;
|
|
525
|
+
timeoutFiles: string[];
|
|
526
|
+
}
|
|
489
527
|
|
|
490
|
-
|
|
491
|
-
|
|
528
|
+
function partitionScores(input: Record<string, number | null>): PartitionedScores {
|
|
529
|
+
const fileScores: Record<string, number> = {};
|
|
530
|
+
const timeoutFiles: string[] = [];
|
|
531
|
+
for (const [file, maybeScore] of Object.entries(input)) {
|
|
532
|
+
if (maybeScore === null) {
|
|
533
|
+
timeoutFiles.push(file);
|
|
534
|
+
} else {
|
|
535
|
+
fileScores[file] = maybeScore;
|
|
536
|
+
}
|
|
492
537
|
}
|
|
493
|
-
|
|
494
|
-
return buildResult('pass', sourceFiles.length, mutationScores, warnings, errors);
|
|
538
|
+
return { fileScores, timeoutFiles };
|
|
495
539
|
}
|
|
496
540
|
|
|
497
541
|
async function collectTestIntentWarnings(sourceFiles: string[], warnings: string[]): Promise<void> {
|
|
@@ -12,10 +12,12 @@ export { registerRunner, resolveRunner, runnerRegistry } from './types';
|
|
|
12
12
|
export { StrykerRunner } from './stryker-runner';
|
|
13
13
|
export { MutmutRunner } from './mutmut-runner';
|
|
14
14
|
export { GoMutantRunner } from './go-mutant-runner';
|
|
15
|
+
export { PitestRunner } from './pitest-runner';
|
|
15
16
|
|
|
16
17
|
import { StrykerRunner } from './stryker-runner';
|
|
17
18
|
import { MutmutRunner } from './mutmut-runner';
|
|
18
19
|
import { GoMutantRunner } from './go-mutant-runner';
|
|
20
|
+
import { PitestRunner } from './pitest-runner';
|
|
19
21
|
import { registerRunner } from './types';
|
|
20
22
|
|
|
21
23
|
/** Auto-register all known runners. */
|
|
@@ -23,4 +25,5 @@ export function registerAllRunners(): void {
|
|
|
23
25
|
registerRunner(new StrykerRunner());
|
|
24
26
|
registerRunner(new MutmutRunner());
|
|
25
27
|
registerRunner(new GoMutantRunner());
|
|
28
|
+
registerRunner(new PitestRunner());
|
|
26
29
|
}
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
// @no-test-required: PITest wrapper — tested via gate-m integration + pre-push Gate M
|
|
2
|
+
import { execSync, spawn } from 'child_process';
|
|
3
|
+
import { existsSync, readFileSync, readdirSync } from 'fs';
|
|
4
|
+
import { join } from 'path';
|
|
5
|
+
import type {
|
|
6
|
+
MutationRunner,
|
|
7
|
+
RunMutationOptions,
|
|
8
|
+
MutationRunOutcome,
|
|
9
|
+
MutationRunResult,
|
|
10
|
+
} from './types';
|
|
11
|
+
|
|
12
|
+
/** JSON shape emitted by PITest when run with -DoutputFormats=JSON */
|
|
13
|
+
interface PitestJsonReport {
|
|
14
|
+
mutations?: {
|
|
15
|
+
/** One of: KILLED, SURVIVED, NO_COVERAGE, TIMED_OUT, NON_VIABLE, MEMORY_ERROR, RUN_ERROR */
|
|
16
|
+
status?: string;
|
|
17
|
+
mutatedClass?: string;
|
|
18
|
+
sourceFile?: string;
|
|
19
|
+
}[];
|
|
20
|
+
/** PITest also writes "totals" in some versions — prefer counting from mutations array */
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* PITest mutation runner for Java and Kotlin files.
|
|
25
|
+
*
|
|
26
|
+
* Supports Maven (pom.xml with pitest-maven plugin) and Gradle
|
|
27
|
+
* (build.gradle/build.gradle.kts with info.solidsoft.pitest plugin).
|
|
28
|
+
*
|
|
29
|
+
* PITest CLI:
|
|
30
|
+
* Maven: mvn org.pitest:pitest-maven:mutationCoverage -DoutputFormats=JSON
|
|
31
|
+
* Gradle: ./gradlew pitest
|
|
32
|
+
*
|
|
33
|
+
* Parses the target/pit-reports/.../mutations.json (Maven) or
|
|
34
|
+
* build/reports/pitest/mutations.json (Gradle) into MutationRunResult.
|
|
35
|
+
*/
|
|
36
|
+
export class PitestRunner implements MutationRunner {
|
|
37
|
+
readonly name = 'pitest';
|
|
38
|
+
readonly extensions = ['java', 'kt', 'kts'];
|
|
39
|
+
|
|
40
|
+
async isAvailable(): Promise<boolean> {
|
|
41
|
+
// Check Maven availability: mvn exists + pom.xml with pitest-maven plugin
|
|
42
|
+
try {
|
|
43
|
+
execSync('mvn --version', { stdio: 'pipe', timeout: 5000 });
|
|
44
|
+
if (existsSync('pom.xml')) {
|
|
45
|
+
const pom = readFileSync('pom.xml', 'utf-8');
|
|
46
|
+
if (
|
|
47
|
+
pom.includes('pitest-maven') ||
|
|
48
|
+
pom.includes('org.pitest')
|
|
49
|
+
) {
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
} catch {
|
|
54
|
+
// Maven not available
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Check Gradle availability: gradle/gradlew exists + build.gradle(.kts) with pitest plugin
|
|
58
|
+
const gradlewPath = './gradlew';
|
|
59
|
+
const hasGradlew = existsSync(gradlewPath);
|
|
60
|
+
try {
|
|
61
|
+
if (!hasGradlew) {
|
|
62
|
+
execSync('gradle --version', { stdio: 'pipe', timeout: 5000 });
|
|
63
|
+
}
|
|
64
|
+
const buildGradle = existsSync('build.gradle.kts')
|
|
65
|
+
? 'build.gradle.kts'
|
|
66
|
+
: existsSync('build.gradle')
|
|
67
|
+
? 'build.gradle'
|
|
68
|
+
: null;
|
|
69
|
+
if (buildGradle) {
|
|
70
|
+
const content = readFileSync(buildGradle, 'utf-8');
|
|
71
|
+
if (
|
|
72
|
+
content.includes('info.solidsoft.pitest') ||
|
|
73
|
+
content.includes('info.solidsoft.gradle.pitest')
|
|
74
|
+
) {
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
} catch {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async run(options: RunMutationOptions): Promise<MutationRunOutcome> {
|
|
86
|
+
const buildTool = this.detectBuildTool(options.cwd);
|
|
87
|
+
|
|
88
|
+
if (buildTool === 'maven') {
|
|
89
|
+
return this.runMaven(options);
|
|
90
|
+
} else if (buildTool === 'gradle') {
|
|
91
|
+
return this.runGradle(options);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
report: null,
|
|
96
|
+
timedOut: false,
|
|
97
|
+
error: 'No supported build tool found (need Maven pom.xml with pitest-maven or Gradle with info.solidsoft.pitest)',
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
private detectBuildTool(cwd: string): 'maven' | 'gradle' | null {
|
|
102
|
+
if (existsSync(join(cwd, 'pom.xml'))) {
|
|
103
|
+
try {
|
|
104
|
+
execSync('mvn --version', { stdio: 'pipe', timeout: 5000 });
|
|
105
|
+
return 'maven';
|
|
106
|
+
} catch {
|
|
107
|
+
// mvn not available
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (
|
|
112
|
+
existsSync(join(cwd, 'build.gradle')) ||
|
|
113
|
+
existsSync(join(cwd, 'build.gradle.kts'))
|
|
114
|
+
) {
|
|
115
|
+
if (existsSync(join(cwd, 'gradlew'))) {
|
|
116
|
+
return 'gradle';
|
|
117
|
+
}
|
|
118
|
+
try {
|
|
119
|
+
execSync('gradle --version', { stdio: 'pipe', timeout: 5000 });
|
|
120
|
+
return 'gradle';
|
|
121
|
+
} catch {
|
|
122
|
+
// gradle not available
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
private runMaven(options: RunMutationOptions): Promise<MutationRunOutcome> {
|
|
130
|
+
return new Promise((resolve) => {
|
|
131
|
+
const child = spawn(
|
|
132
|
+
'mvn',
|
|
133
|
+
[
|
|
134
|
+
'test-compile',
|
|
135
|
+
'org.pitest:pitest-maven:mutationCoverage',
|
|
136
|
+
'-DoutputFormats=JSON',
|
|
137
|
+
],
|
|
138
|
+
{
|
|
139
|
+
stdio: 'pipe',
|
|
140
|
+
shell: false,
|
|
141
|
+
cwd: options.cwd,
|
|
142
|
+
},
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
let stderr = '';
|
|
146
|
+
let stdout = '';
|
|
147
|
+
|
|
148
|
+
child.stdout?.on('data', (data: Buffer) => {
|
|
149
|
+
stdout += data.toString();
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
child.stderr?.on('data', (data: Buffer) => {
|
|
153
|
+
stderr += data.toString();
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
const timeoutId = setTimeout(() => {
|
|
157
|
+
child.kill('SIGTERM');
|
|
158
|
+
setTimeout(() => {
|
|
159
|
+
if (!child.killed) child.kill('SIGKILL');
|
|
160
|
+
}, 5000);
|
|
161
|
+
}, options.timeoutMs);
|
|
162
|
+
|
|
163
|
+
child.on('close', (code) => {
|
|
164
|
+
clearTimeout(timeoutId);
|
|
165
|
+
|
|
166
|
+
if (code === null) {
|
|
167
|
+
resolve({ report: null, timedOut: true });
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const report = this.parsePitestJson(options.cwd, 'maven');
|
|
172
|
+
resolve({
|
|
173
|
+
report,
|
|
174
|
+
timedOut: false,
|
|
175
|
+
error: code !== 0 ? stderr || stdout : undefined,
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
child.on('error', (err) => {
|
|
180
|
+
clearTimeout(timeoutId);
|
|
181
|
+
resolve({ report: null, timedOut: false, error: err.message });
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
private runGradle(options: RunMutationOptions): Promise<MutationRunOutcome> {
|
|
187
|
+
return new Promise((resolve) => {
|
|
188
|
+
const gradlewPath = join(options.cwd, 'gradlew');
|
|
189
|
+
const gradleCmd = existsSync(gradlewPath) ? gradlewPath : 'gradle';
|
|
190
|
+
|
|
191
|
+
const child = spawn(gradleCmd, ['pitest'], {
|
|
192
|
+
stdio: 'pipe',
|
|
193
|
+
shell: false,
|
|
194
|
+
cwd: options.cwd,
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
let stderr = '';
|
|
198
|
+
let stdout = '';
|
|
199
|
+
|
|
200
|
+
child.stdout?.on('data', (data: Buffer) => {
|
|
201
|
+
stdout += data.toString();
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
child.stderr?.on('data', (data: Buffer) => {
|
|
205
|
+
stderr += data.toString();
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
const timeoutId = setTimeout(() => {
|
|
209
|
+
child.kill('SIGTERM');
|
|
210
|
+
setTimeout(() => {
|
|
211
|
+
if (!child.killed) child.kill('SIGKILL');
|
|
212
|
+
}, 5000);
|
|
213
|
+
}, options.timeoutMs);
|
|
214
|
+
|
|
215
|
+
child.on('close', (code) => {
|
|
216
|
+
clearTimeout(timeoutId);
|
|
217
|
+
|
|
218
|
+
if (code === null) {
|
|
219
|
+
resolve({ report: null, timedOut: true });
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const report = this.parsePitestJson(options.cwd, 'gradle');
|
|
224
|
+
resolve({
|
|
225
|
+
report,
|
|
226
|
+
timedOut: false,
|
|
227
|
+
error: code !== 0 ? stderr || stdout : undefined,
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
child.on('error', (err) => {
|
|
232
|
+
clearTimeout(timeoutId);
|
|
233
|
+
resolve({ report: null, timedOut: false, error: err.message });
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
private findMavenReportPath(cwd: string): string | null {
|
|
239
|
+
const reportsDir = join(cwd, 'target', 'pit-reports');
|
|
240
|
+
if (!existsSync(reportsDir)) return null;
|
|
241
|
+
for (const entry of readdirSync(reportsDir)) {
|
|
242
|
+
const candidate = join(reportsDir, entry, 'mutations.json');
|
|
243
|
+
if (existsSync(candidate)) return candidate;
|
|
244
|
+
}
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
private findGradleReportPath(cwd: string): string | null {
|
|
249
|
+
const candidate = join(cwd, 'build', 'reports', 'pitest', 'mutations.json');
|
|
250
|
+
return existsSync(candidate) ? candidate : null;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
private computeScore(data: PitestJsonReport): MutationRunResult | null {
|
|
254
|
+
if (!data.mutations || data.mutations.length === 0) return null;
|
|
255
|
+
const nrOfKilledMutants = data.mutations.filter(
|
|
256
|
+
(m) => m.status === 'KILLED',
|
|
257
|
+
).length;
|
|
258
|
+
const nrOfMutants = data.mutations.length;
|
|
259
|
+
const nrOfSurvivedMutants = nrOfMutants - nrOfKilledMutants;
|
|
260
|
+
const mutationScore = nrOfMutants === 0
|
|
261
|
+
? 0
|
|
262
|
+
: (nrOfKilledMutants / nrOfMutants) * 100;
|
|
263
|
+
return { mutationScore, nrOfMutants, nrOfKilledMutants, nrOfSurvivedMutants };
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Locate and parse the PITest JSON report file.
|
|
268
|
+
*
|
|
269
|
+
* Maven writes mutations.json under a timestamped subdirectory:
|
|
270
|
+
* target/pit-reports/<YYYYMMDDHHMM>/mutations.json
|
|
271
|
+
* This method scans all subdirectories to find the latest report.
|
|
272
|
+
*
|
|
273
|
+
* Gradle writes to: build/reports/pitest/mutations.json
|
|
274
|
+
*/
|
|
275
|
+
private parsePitestJson(
|
|
276
|
+
cwd: string,
|
|
277
|
+
buildTool: 'maven' | 'gradle',
|
|
278
|
+
): MutationRunResult | null {
|
|
279
|
+
try {
|
|
280
|
+
const jsonPath = buildTool === 'maven'
|
|
281
|
+
? this.findMavenReportPath(cwd)
|
|
282
|
+
: this.findGradleReportPath(cwd);
|
|
283
|
+
if (!jsonPath) return null;
|
|
284
|
+
const data: PitestJsonReport = JSON.parse(readFileSync(jsonPath, 'utf-8'));
|
|
285
|
+
return this.computeScore(data);
|
|
286
|
+
} catch {
|
|
287
|
+
return null;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "xp-gate",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.4",
|
|
4
4
|
"displayName": "XP-Gate",
|
|
5
5
|
"description": "Extreme Programming quality gates + AI workflow skills for Claude Code. Includes 10 quality gates (Gate 0-9), Sprint Flow (11 phases), and Delphi multi-expert review (>=90% consensus).",
|
|
6
6
|
"author": {
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# SKILLS/DELPHI-REVIEW KNOWLEDGE BASE
|
|
2
2
|
|
|
3
|
-
**Generated:** 2026-
|
|
4
|
-
**Commit:**
|
|
5
|
-
**Branch:**
|
|
6
|
-
**Version:** 0.11.
|
|
3
|
+
**Generated:** 2026-07-01
|
|
4
|
+
**Commit:** a5ef11d
|
|
5
|
+
**Branch:** sprint/2026-06-30-01
|
|
6
|
+
**Version:** 0.11.4.0
|
|
7
7
|
|
|
8
8
|
## OVERVIEW
|
|
9
9
|
Delphi Consensus Review — multi-round anonymous expert review (≥90% threshold, 3 experts from ≥2 providers, domestic models only). Supports design + code-walkthrough modes.
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# SKILLS/SPRINT-FLOW KNOWLEDGE BASE
|
|
2
2
|
|
|
3
|
-
**Generated:** 2026-
|
|
4
|
-
**Commit:**
|
|
5
|
-
**Branch:**
|
|
6
|
-
**Version:** 0.11.
|
|
3
|
+
**Generated:** 2026-07-01
|
|
4
|
+
**Commit:** a5ef11d
|
|
5
|
+
**Branch:** sprint/2026-06-30-01
|
|
6
|
+
**Version:** 0.11.4.0
|
|
7
7
|
|
|
8
8
|
## OVERVIEW
|
|
9
9
|
**11-phase** development pipeline: ISOLATE → AUTO-ESTIMATE → THINK → PLAN → BUILD → REVIEW → SHIP → LAND → USER ACCEPTANCE → FEEDBACK → CLEANUP. Phase 2 default build mode is **ralph-loop** (REQ-level iteration, 40-67% token savings vs parallel). HARD-GATE in Phase 1: design must pass Delphi review (≥90% consensus) before any coding.
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# SKILLS/TEST-SPECIFICATION-ALIGNMENT KNOWLEDGE BASE
|
|
2
2
|
|
|
3
|
-
**Generated:** 2026-
|
|
4
|
-
**Commit:**
|
|
5
|
-
**Branch:**
|
|
6
|
-
**Version:** 0.11.
|
|
3
|
+
**Generated:** 2026-07-01
|
|
4
|
+
**Commit:** a5ef11d
|
|
5
|
+
**Branch:** sprint/2026-06-30-01
|
|
6
|
+
**Version:** 0.11.4.0
|
|
7
7
|
|
|
8
8
|
## OVERVIEW
|
|
9
9
|
Test-Specification Alignment Engine — two-stage validation ensuring tests accurately reflect requirements and design specs.
|