@zohodesk/codestandard-validator 1.1.4 → 1.2.4-exp-2

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.
Files changed (36) hide show
  1. package/bin/cliCI.js +0 -0
  2. package/build/ai/config.example.json +10 -0
  3. package/build/ai/ollama-service.js +403 -0
  4. package/build/ai/prompts/CODE_REVIEW_PROMPT.md +72 -0
  5. package/build/ai/prompts/PROMPT1.MD +70 -0
  6. package/build/ai/prompts/PROMPT2.md +159 -0
  7. package/build/ai/prompts/PROMPT3.md +64 -0
  8. package/build/ai/prompts/PROMPT4.md +64 -0
  9. package/build/ai/provider-factory.js +19 -0
  10. package/build/ai/providers/OllamaProvider.js +106 -0
  11. package/build/ai/render.js +157 -0
  12. package/build/ai/run-review.js +50 -0
  13. package/build/chunk/chunk_Restriction.js +202 -0
  14. package/build/hooks/Precommit/pre-commit-default.js +158 -140
  15. package/build/hooks/hook.js +6 -5
  16. package/build/lib/postinstall.js +6 -10
  17. package/build/mutation/branchDiff.js +178 -0
  18. package/build/mutation/fileResolver.js +170 -0
  19. package/build/mutation/index.js +16 -0
  20. package/build/mutation/mutatePattern.json +3 -0
  21. package/build/mutation/mutationCli.js +111 -0
  22. package/build/mutation/mutationRunner.js +208 -0
  23. package/build/mutation/reportGenerator.js +72 -0
  24. package/build/mutation/strykerWrapper.js +180 -0
  25. package/build/utils/FileAndFolderOperations/filterFiles.js +8 -6
  26. package/build/utils/FileAndFolderOperations/removeFolder.js +2 -2
  27. package/build/utils/General/Config.js +25 -0
  28. package/build/utils/General/SonarQubeUtil.js +1 -1
  29. package/jest.config.js +1 -1
  30. package/package.json +4 -1
  31. package/samples/sample-branch-mode.js +34 -0
  32. package/samples/sample-cli-entry.js +34 -0
  33. package/samples/sample-components.js +63 -0
  34. package/samples/sample-directory-mode.js +30 -0
  35. package/samples/sample-runner-direct.js +32 -0
  36. package/samples/sample-with-api.js +44 -0
@@ -0,0 +1,178 @@
1
+ "use strict";
2
+
3
+ const {
4
+ execSync
5
+ } = require('child_process');
6
+ const path = require('path');
7
+ class BranchDiff {
8
+ constructor(options = {}) {
9
+ this._cwd = options.cwd || process.cwd();
10
+ this._api = options.api || null;
11
+ this._patToken = options.patToken || null;
12
+ this._sourceExtensions = options.sourceExtensions || ['.js', '.ts', '.jsx', '.tsx', '.mjs', '.cjs'];
13
+ this._testPattern = options.testPattern || /\.(test)\.(js|ts|jsx|tsx|mjs|cjs)$/;
14
+ }
15
+ _getCurrentBranch() {
16
+ try {
17
+ return execSync('git rev-parse --abbrev-ref HEAD', {
18
+ cwd: this._cwd,
19
+ encoding: 'utf-8'
20
+ }).trim();
21
+ } catch (err) {
22
+ throw new Error(`Failed to get current branch: ${err.message}`);
23
+ }
24
+ }
25
+ _fetchBranch(branch) {
26
+ try {
27
+ execSync(`git fetch origin ${branch}`, {
28
+ cwd: this._cwd,
29
+ encoding: 'utf-8',
30
+ stdio: 'pipe'
31
+ });
32
+ } catch (err) {
33
+ // Branch might already be available locally
34
+ }
35
+ }
36
+ _resolveBranchRef(branch) {
37
+ // Check if the ref exists locally
38
+ const candidates = [branch, `origin/${branch}`];
39
+ for (const ref of candidates) {
40
+ try {
41
+ execSync(`git rev-parse --verify ${ref}`, {
42
+ cwd: this._cwd,
43
+ encoding: 'utf-8',
44
+ stdio: 'pipe'
45
+ });
46
+ return ref;
47
+ } catch {
48
+ // try next
49
+ }
50
+ }
51
+ throw new Error(`Branch '${branch}' not found locally or as 'origin/${branch}'. ` + `Run 'git fetch origin ${branch}' or check the branch name.`);
52
+ }
53
+ _getDiffFiles(targetBranch) {
54
+ const currentBranch = this._getCurrentBranch();
55
+ this._fetchBranch(targetBranch);
56
+ const resolvedRef = this._resolveBranchRef(targetBranch);
57
+ let diffRef;
58
+ try {
59
+ const mergeBase = execSync(`git merge-base ${resolvedRef} ${currentBranch}`, {
60
+ cwd: this._cwd,
61
+ encoding: 'utf-8'
62
+ }).trim();
63
+ diffRef = mergeBase;
64
+ } catch {
65
+ diffRef = resolvedRef;
66
+ }
67
+ try {
68
+ const output = execSync(`git diff --name-only --diff-filter=ACMR ${diffRef}`, {
69
+ cwd: this._cwd,
70
+ encoding: 'utf-8'
71
+ });
72
+ return output.split('\n').map(f => f.trim()).filter(f => f.length > 0);
73
+ } catch (err) {
74
+ throw new Error(`Failed to get diff files against branch '${targetBranch}': ${err.message}`);
75
+ }
76
+ }
77
+ _getDiffFilesFromApi(targetBranch) {
78
+ if (!this._api || !this._patToken) {
79
+ throw new Error('API and PAT token are required for API-based diff');
80
+ }
81
+ // Placeholder for API-based diff (e.g., Azure DevOps, GitHub, etc.)
82
+ // Consumers can pass in their own api object that implements `getChangedFiles(targetBranch, patToken)`
83
+ if (typeof this._api.getChangedFiles === 'function') {
84
+ return this._api.getChangedFiles(targetBranch, this._patToken);
85
+ }
86
+ throw new Error('API object must implement getChangedFiles(targetBranch, patToken)');
87
+ }
88
+
89
+ /**
90
+ * Fetches branch diff from GitLab compare API via HTTPS
91
+ * @param {string} hostname - GitLab host (e.g. 'gitlab.example.com')
92
+ * @param {string} branch - Target branch to compare against
93
+ * @returns {Promise<string[]>} - Array of changed file paths
94
+ */
95
+ _fetchBranchDiffFromApi(hostname, branch, projectId) {
96
+ const currentBranch = this._getCurrentBranch();
97
+ const patToken = this._patToken || process.env.ZGIT_TOKEN;
98
+ if (!patToken) {
99
+ return reject(new Error('PAT token is required. Set it via options.patToken or ZGIT_TOKEN env var'));
100
+ }
101
+ if (!projectId) {
102
+ return reject(new Error('GitLab projectId is required for API-based diff'));
103
+ }
104
+ const createBranchDiffChunk = (resolve, reject) => {
105
+ // code duplicate need find solution
106
+ var command;
107
+ const endPoint = `${hostname}/api/v4/projects/${encodeURIComponent(projectId)}/repository/compare?from=${branch}&to=${currentBranch}`;
108
+ try {
109
+ command = `curl --header "PRIVATE-TOKEN:${patToken}" "${endPoint}"`;
110
+ return resolve(parserReleaseDiffRemoteAPI(JSON.parse(execSync(command, {
111
+ cwd: this._cwd
112
+ }))));
113
+ } catch (error) {
114
+ Logger.log(error);
115
+ Logger.log(`\n INFO : If you are using a VPN and encounter an SSL certification issue, ensure that the proxy is enabled for SSH and shell connections.`);
116
+ Logger.log(`\n Make sure that you have access to this repository`);
117
+ reject(error);
118
+ }
119
+ };
120
+ function parserReleaseDiffRemoteAPI(rawresponse) {
121
+ var formatted = [];
122
+ if (rawresponse !== null && rawresponse !== void 0 && rawresponse.diffs) {
123
+ formatted = rawresponse.diffs.map(changeset => {
124
+ return changeset === null || changeset === void 0 ? void 0 : changeset.new_path;
125
+ }).filter(Boolean);
126
+ }
127
+ return formatted;
128
+ }
129
+ return new Promise((resolve, reject) => {
130
+ return createBranchDiffChunk(resolve, reject);
131
+ });
132
+ }
133
+ async getStagedFiles() {
134
+ try {
135
+ const stdout = await execSync("git diff --staged --name-only").toString();
136
+ const files = stdout.trim().split("\n").filter(Boolean);
137
+ return files;
138
+ } catch (error) {
139
+ console.log(error);
140
+ throw new Error("Couldn't fetch staged files");
141
+ }
142
+ }
143
+ async getChangedFiles(targetBranch, options = {}) {
144
+ let allFiles;
145
+ if (options.useApi && options.hostname && options.projectId) {
146
+ allFiles = await this._fetchBranchDiffFromApi(options.hostname, targetBranch, options.projectId).concat(await this.getStagedFiles());
147
+ } else {
148
+ allFiles = this._getDiffFiles(targetBranch);
149
+ }
150
+ return this._categorizeFiles(allFiles);
151
+ }
152
+ _isSourceFile(filePath) {
153
+ const ext = path.extname(filePath);
154
+ if (!this._sourceExtensions.includes(ext)) {
155
+ return false;
156
+ }
157
+ return !this._testPattern.test(filePath);
158
+ }
159
+ _isTestFile(filePath) {
160
+ return this._testPattern.test(filePath);
161
+ }
162
+ _categorizeFiles(files) {
163
+ const sourceFiles = [];
164
+ const testFiles = [];
165
+ for (const file of files) {
166
+ if (this._isTestFile(file)) {
167
+ testFiles.push(file);
168
+ } else if (this._isSourceFile(file)) {
169
+ sourceFiles.push(file);
170
+ }
171
+ }
172
+ return {
173
+ sourceFiles,
174
+ testFiles
175
+ };
176
+ }
177
+ }
178
+ module.exports = BranchDiff;
@@ -0,0 +1,170 @@
1
+ "use strict";
2
+
3
+ const path = require('path');
4
+ const fs = require('fs');
5
+ const glob = require('glob');
6
+ const {
7
+ getRootDirectory
8
+ } = require('../utils/General/RootDirectoryUtils/getRootDirectory');
9
+ class FileResolver {
10
+ constructor(options = {}) {
11
+ this._rootdir = getRootDirectory();
12
+ this._cwd = options.cwd || process.cwd();
13
+ this._testSuffixes = options.testSuffixes || ['.test.js', '.test.ts', '.test.jsx', '.test.tsx'];
14
+ this._sourceExtensions = options.sourceExtensions || ['.js', '.ts', '.jsx', '.tsx', '.mjs', '.cjs'];
15
+ }
16
+ findTestForSource(sourceFile) {
17
+ const parsed = path.parse(sourceFile);
18
+ const dir = parsed.dir;
19
+ const name = parsed.name;
20
+
21
+ // Strategy 1: Test file alongside source (e.g., src/foo.js -> src/foo.test.js)
22
+ for (const suffix of this._testSuffixes) {
23
+ const candidate = path.join(dir, `${name}${suffix}`);
24
+ if (fs.existsSync(path.resolve(this._rootdir, candidate))) {
25
+ return candidate;
26
+ }
27
+ }
28
+
29
+ // Strategy 2: Test in __tests__ directory (e.g., src/foo.js -> src/__tests__/foo.test.js)
30
+ for (const suffix of this._testSuffixes) {
31
+ const candidate = path.join(dir, '__tests__', `${name}${suffix}`);
32
+ if (fs.existsSync(path.resolve(this._rootdir, candidate))) {
33
+ return candidate;
34
+ }
35
+ }
36
+
37
+ // Strategy 3: Mirror structure under test/tests directory
38
+ const testDirs = ['test', 'tests', '__tests__'];
39
+ const srcDirs = ['src', 'lib'];
40
+ for (const srcDir of srcDirs) {
41
+ if (dir.startsWith(srcDir)) {
42
+ const relativePath = dir.substring(srcDir.length);
43
+ for (const testDir of testDirs) {
44
+ for (const suffix of this._testSuffixes) {
45
+ const candidate = path.join(testDir, relativePath, `${name}${suffix}`);
46
+ if (fs.existsSync(path.resolve(this._rootdir, candidate))) {
47
+ return candidate;
48
+ }
49
+ }
50
+ }
51
+ }
52
+ }
53
+ return null;
54
+ }
55
+ findSourceForTest(testFile) {
56
+ const parsed = path.parse(testFile);
57
+ const dir = parsed.dir;
58
+ let name = parsed.name;
59
+
60
+ // Remove .test suffix
61
+ for (const suffix of ['.test', '.spec']) {
62
+ if (name.endsWith(suffix)) {
63
+ name = name.slice(0, -suffix.length);
64
+ break;
65
+ }
66
+ }
67
+
68
+ // Strategy 1: Source file alongside test
69
+ for (const ext of this._sourceExtensions) {
70
+ const candidate = path.join(dir, `${name}${ext}`);
71
+ if (fs.existsSync(path.resolve(this._rootdir, candidate))) {
72
+ return candidate;
73
+ }
74
+ }
75
+
76
+ // Strategy 2: Source in parent (when test is in __tests__)
77
+ if (dir.includes('__tests__')) {
78
+ const parentDir = dir.replace(/__tests__\/?/, '');
79
+ for (const ext of this._sourceExtensions) {
80
+ const candidate = path.join(parentDir, `${name}${ext}`);
81
+ if (fs.existsSync(path.resolve(this._rootdir, candidate))) {
82
+ return candidate;
83
+ }
84
+ }
85
+ }
86
+
87
+ // Strategy 3: Mirror test dir structure back to src/lib
88
+ const testDirs = ['test', 'tests', '__tests__'];
89
+ const srcDirs = ['src', 'lib'];
90
+ for (const testDir of testDirs) {
91
+ if (dir.startsWith(testDir)) {
92
+ const relativePath = dir.substring(testDir.length);
93
+ for (const srcDir of srcDirs) {
94
+ for (const ext of this._sourceExtensions) {
95
+ const candidate = path.join(srcDir, relativePath, `${name}${ext}`);
96
+ if (fs.existsSync(path.resolve(this._rootdir, candidate))) {
97
+ return candidate;
98
+ }
99
+ }
100
+ }
101
+ }
102
+ }
103
+ return null;
104
+ }
105
+ resolveSourceTestPairs(sourceFiles, testFiles) {
106
+ const pairs = [];
107
+ const resolvedSources = new Set();
108
+ const resolvedTests = new Set();
109
+
110
+ // Map source -> test
111
+ for (const source of sourceFiles) {
112
+ const normalizedSource = source.replace(/\\/g, '/');
113
+ const test = this.findTestForSource(normalizedSource);
114
+ if (test) {
115
+ const normalizedTest = test.replace(/\\/g, '/');
116
+ pairs.push({
117
+ source: normalizedSource,
118
+ test: normalizedTest
119
+ });
120
+ resolvedSources.add(normalizedSource);
121
+ resolvedTests.add(normalizedTest);
122
+ } else {
123
+ // Source file with no matching test
124
+ pairs.push({
125
+ source: normalizedSource,
126
+ test: null
127
+ });
128
+ resolvedSources.add(normalizedSource);
129
+ }
130
+ }
131
+
132
+ // Map remaining test -> source
133
+ for (const test of testFiles) {
134
+ const normalizedTest = test.replace(/\\/g, '/');
135
+ if (resolvedTests.has(normalizedTest)) continue;
136
+ const source = this.findSourceForTest(normalizedTest);
137
+ if (source && !resolvedSources.has(source.replace(/\\/g, '/'))) {
138
+ pairs.push({
139
+ source: source.replace(/\\/g, '/'),
140
+ test: normalizedTest
141
+ });
142
+ resolvedSources.add(source.replace(/\\/g, '/'));
143
+ }
144
+ resolvedTests.add(normalizedTest);
145
+ }
146
+ return pairs;
147
+ }
148
+ collectFiles(directory, pattern) {
149
+ const fullPattern = path.join(directory, pattern || '**/*.{js,ts,jsx,tsx}').replace(/\\/g, '/');
150
+ try {
151
+ const files = glob.sync(fullPattern, {
152
+ cwd: this._rootdir,
153
+ nodir: true
154
+ });
155
+ return files;
156
+ } catch (err) {
157
+ throw new Error(`Failed to collect files from '${directory}': ${err.message}`);
158
+ }
159
+ }
160
+ collectSourceFiles(srcDir) {
161
+ const allFiles = this.collectFiles(srcDir, '**/*.{js,ts,jsx,tsx,mjs,cjs}');
162
+ const testPattern = /\.(test|spec)\.(js|ts|jsx|tsx|mjs|cjs)$/;
163
+ return allFiles.filter(f => !testPattern.test(f));
164
+ }
165
+ collectTestFiles(testDir) {
166
+ const allFiles = this.collectFiles(testDir, '**/*.{test,spec}.{js,ts,jsx,tsx}');
167
+ return allFiles;
168
+ }
169
+ }
170
+ module.exports = FileResolver;
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+
3
+ const MutationRunner = require('./mutationRunner');
4
+ const MutationCli = require('./mutationCli');
5
+ const StrykerWrapper = require('./strykerWrapper');
6
+ const BranchDiff = require('./branchDiff');
7
+ const FileResolver = require('./fileResolver');
8
+ const ReportGenerator = require('./reportGenerator');
9
+ module.exports = {
10
+ MutationRunner,
11
+ MutationCli,
12
+ StrykerWrapper,
13
+ BranchDiff,
14
+ FileResolver,
15
+ ReportGenerator
16
+ };
@@ -0,0 +1,3 @@
1
+ {
2
+ "defaultPattern": "src/**/*.js"
3
+ }
@@ -0,0 +1,111 @@
1
+ "use strict";
2
+
3
+ const MutationRunner = require('./mutationRunner');
4
+ class MutationCli {
5
+ constructor(options = {}) {
6
+ this._options = options;
7
+ }
8
+ parseArgs(args) {
9
+ const parsed = {
10
+ command: null,
11
+ branch: null,
12
+ src: null,
13
+ test: null,
14
+ useApi: false,
15
+ outputDir: null,
16
+ outputFileName: null,
17
+ logLevel: 'info',
18
+ hostname: null
19
+ };
20
+ for (const arg of args) {
21
+ if (arg === 'mutate') {
22
+ parsed.command = 'mutate';
23
+ } else if (arg.startsWith('--branch=')) {
24
+ parsed.branch = arg.split('=')[1];
25
+ } else if (arg.startsWith('--src=')) {
26
+ parsed.src = arg.split('=')[1];
27
+ } else if (arg.startsWith('--test=')) {
28
+ parsed.test = arg.split('=')[1];
29
+ } else if (arg === '--useApi') {
30
+ parsed.useApi = true;
31
+ } else if (arg.startsWith('--outputDir=')) {
32
+ parsed.outputDir = arg.split('=')[1];
33
+ } else if (arg.startsWith('--outputFileName=')) {
34
+ parsed.outputFileName = arg.split('=')[1];
35
+ } else if (arg.startsWith('--logLevel=')) {
36
+ parsed.logLevel = arg.split('=')[1];
37
+ } else if (arg.startsWith('--hostname=')) {
38
+ parsed.hostname = arg.split('=')[1];
39
+ }
40
+ }
41
+ return parsed;
42
+ }
43
+ validate(parsed) {
44
+ if (parsed.command !== 'mutate') {
45
+ return {
46
+ valid: false,
47
+ error: 'Unknown command. Expected "mutate".'
48
+ };
49
+ }
50
+ const hasBranch = !!parsed.branch;
51
+ const hasSrcTest = !!parsed.src && !!parsed.test;
52
+ if (!hasBranch && !hasSrcTest) {
53
+ return {
54
+ valid: false,
55
+ error: 'Missing arguments. Usage:\n' + ' npx ZDLintFramework mutate --branch=<branch>\n' + ' npx ZDLintFramework mutate --src=<sourcedir> --test=<testdir>'
56
+ };
57
+ }
58
+ if (hasBranch && hasSrcTest) {
59
+ return {
60
+ valid: false,
61
+ error: 'Cannot use both --branch and --src/--test together. Choose one mode.'
62
+ };
63
+ }
64
+ return {
65
+ valid: true
66
+ };
67
+ }
68
+ async execute(args) {
69
+ const parsed = this.parseArgs(args);
70
+ const validation = this.validate(parsed);
71
+ if (!validation.valid) {
72
+ return {
73
+ success: false,
74
+ error: validation.error
75
+ };
76
+ }
77
+ const runner = new MutationRunner({
78
+ cwd: this._options.cwd,
79
+ api: this._options.api,
80
+ patToken: this._options.patToken,
81
+ jestConfig: this._options.jestConfig,
82
+ outputDir: parsed.outputDir || this._options.outputDir,
83
+ outputFileName: parsed.outputFileName || this._options.outputFileName,
84
+ logLevel: parsed.logLevel,
85
+ concurrency: this._options.concurrency,
86
+ timeoutMS: this._options.timeoutMS
87
+ });
88
+ try {
89
+ let result;
90
+ if (parsed.branch) {
91
+ result = await runner.runByBranch(parsed.branch, {
92
+ useApi: parsed.useApi,
93
+ hostname: this._options.hostname,
94
+ projectId: this._options.projectId
95
+ });
96
+ } else {
97
+ result = await runner.runByDirectory(parsed.src, parsed.test);
98
+ }
99
+ return {
100
+ success: true,
101
+ ...result
102
+ };
103
+ } catch (err) {
104
+ return {
105
+ success: false,
106
+ error: err.message
107
+ };
108
+ }
109
+ }
110
+ }
111
+ module.exports = MutationCli;
@@ -0,0 +1,208 @@
1
+ "use strict";
2
+
3
+ const path = require('path');
4
+ const StrykerWrapper = require('./strykerWrapper');
5
+ const BranchDiff = require('./branchDiff');
6
+ const FileResolver = require('./fileResolver');
7
+ const ReportGenerator = require('./reportGenerator');
8
+ class MutationRunner {
9
+ constructor(options = {}) {
10
+ this._cwd = options.cwd || process.cwd();
11
+ this._stryker = new StrykerWrapper({
12
+ reportPath: options.reportPath,
13
+ concurrency: options.concurrency,
14
+ timeoutMS: options.timeoutMS,
15
+ logLevel: options.logLevel
16
+ });
17
+ this._branchDiff = new BranchDiff({
18
+ cwd: this._cwd,
19
+ api: options.api || null,
20
+ patToken: options.patToken || null
21
+ });
22
+ this._fileResolver = new FileResolver({
23
+ cwd: this._cwd
24
+ });
25
+ this._reportGenerator = new ReportGenerator({
26
+ cwd: this._cwd,
27
+ outputDir: options.outputDir || 'reports/mutation',
28
+ outputFileName: options.outputFileName || 'mutation-report.json'
29
+ });
30
+ this._jestConfig = options.jestConfig || {};
31
+ }
32
+ async runByBranch(branch, options = {}) {
33
+ if (!branch) {
34
+ throw new Error('Branch name is required. Usage: npx ZDLintFramework mutate --branch=<branch>');
35
+ }
36
+
37
+ // Step 1: Get changed files
38
+ const {
39
+ sourceFiles,
40
+ testFiles
41
+ } = await this._branchDiff.getChangedFiles(branch, {
42
+ useApi: options.useApi || false,
43
+ hostname: options.hostname || null,
44
+ projectId: options.projectId || null
45
+ });
46
+ if (sourceFiles.length === 0 && testFiles.length === 0) {
47
+ const emptyResult = this._buildEmptyResult();
48
+ const {
49
+ report,
50
+ filePath
51
+ } = this._reportGenerator.generateAndWrite(emptyResult, {
52
+ command: `mutate --branch=${branch}`,
53
+ branch,
54
+ filePairs: []
55
+ });
56
+ return {
57
+ report,
58
+ filePath,
59
+ message: 'No changed source or test files found in branch diff.'
60
+ };
61
+ }
62
+
63
+ // Step 2: Resolve source-test pairs
64
+ const pairs = this._fileResolver.resolveSourceTestPairs(sourceFiles, testFiles);
65
+ const validPairs = pairs.filter(p => p.source && p.test);
66
+ const sourcesToMutate = [...new Set(validPairs.map(p => p.source))];
67
+ const testsToRun = [...new Set(validPairs.map(p => p.test))];
68
+ if (sourcesToMutate.length === 0) {
69
+ const emptyResult = this._buildEmptyResult();
70
+ const {
71
+ report,
72
+ filePath
73
+ } = this._reportGenerator.generateAndWrite(emptyResult, {
74
+ command: `mutate --branch=${branch}`,
75
+ branch,
76
+ filePairs: pairs
77
+ });
78
+ return {
79
+ report,
80
+ filePath,
81
+ message: 'No source files with corresponding test files found.'
82
+ };
83
+ }
84
+
85
+ // Step 3: Run Stryker mutation testing
86
+ const mutationResult = await this._stryker.run(sourcesToMutate, {
87
+ testFiles: testsToRun,
88
+ jest: {
89
+ ...this._jestConfig,
90
+ enableFindRelatedTests: true
91
+ }
92
+ });
93
+
94
+ // Step 4: Generate report
95
+ const {
96
+ report,
97
+ filePath
98
+ } = this._reportGenerator.generateAndWrite(mutationResult, {
99
+ command: `mutate --branch=${branch}`,
100
+ branch,
101
+ filePairs: pairs
102
+ });
103
+ return {
104
+ report,
105
+ filePath
106
+ };
107
+ }
108
+ async runByDirectory(srcDir, testDir, options = {}) {
109
+ if (!srcDir || !testDir) {
110
+ throw new Error('Both source and test directories are required. Usage: npx ZDLintFramework mutate --src=<sourcedir> --test=<testdir>');
111
+ }
112
+
113
+ // Step 1: Collect all source and test files
114
+ const sourceFiles = this._fileResolver.collectSourceFiles(srcDir);
115
+ const testFiles = this._fileResolver.collectTestFiles(testDir);
116
+ if (sourceFiles.length === 0) {
117
+ const emptyResult = this._buildEmptyResult();
118
+ const {
119
+ report,
120
+ filePath
121
+ } = this._reportGenerator.generateAndWrite(emptyResult, {
122
+ command: `mutate --src=${srcDir} --test=${testDir}`,
123
+ srcDir,
124
+ testDir,
125
+ filePairs: []
126
+ });
127
+ return {
128
+ report,
129
+ filePath,
130
+ message: 'No source files found in the specified directory.'
131
+ };
132
+ }
133
+
134
+ // Step 2: Resolve source-test pairs
135
+ const pairs = this._fileResolver.resolveSourceTestPairs(sourceFiles, testFiles);
136
+ const validPairs = pairs.filter(p => p.source && p.test);
137
+ const sourcesToMutate = [...new Set(validPairs.map(p => p.source))];
138
+ const testsToRun = [...new Set(validPairs.map(p => p.test))];
139
+ if (sourcesToMutate.length === 0) {
140
+ // Fallback: mutate all source files with all test files
141
+ const mutationResult = await this._stryker.run(sourceFiles, {
142
+ testFiles,
143
+ jest: {
144
+ ...this._jestConfig,
145
+ enableFindRelatedTests: true
146
+ }
147
+ });
148
+ const {
149
+ report,
150
+ filePath
151
+ } = this._reportGenerator.generateAndWrite(mutationResult, {
152
+ command: `mutate --src=${srcDir} --test=${testDir}`,
153
+ srcDir,
154
+ testDir,
155
+ filePairs: sourceFiles.map(s => ({
156
+ source: s,
157
+ test: null
158
+ }))
159
+ });
160
+ return {
161
+ report,
162
+ filePath
163
+ };
164
+ }
165
+
166
+ // Step 3: Run Stryker mutation testing
167
+ const mutationResult = await this._stryker.run(sourcesToMutate, {
168
+ testFiles: testsToRun,
169
+ jest: {
170
+ ...this._jestConfig,
171
+ enableFindRelatedTests: true
172
+ }
173
+ });
174
+
175
+ // Step 4: Generate report
176
+ const {
177
+ report,
178
+ filePath
179
+ } = this._reportGenerator.generateAndWrite(mutationResult, {
180
+ command: `mutate --src=${srcDir} --test=${testDir}`,
181
+ srcDir,
182
+ testDir,
183
+ filePairs: pairs
184
+ });
185
+ return {
186
+ report,
187
+ filePath
188
+ };
189
+ }
190
+ _buildEmptyResult() {
191
+ return {
192
+ files: {},
193
+ mutants: [],
194
+ summary: {
195
+ totalMutants: 0,
196
+ killed: 0,
197
+ survived: 0,
198
+ timeout: 0,
199
+ noCoverage: 0,
200
+ ignored: 0,
201
+ runtimeErrors: 0,
202
+ compileErrors: 0,
203
+ mutationScore: 0
204
+ }
205
+ };
206
+ }
207
+ }
208
+ module.exports = MutationRunner;