x-fidelity 3.5.0 → 3.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,17 @@
1
+ # [3.6.0](https://github.com/zotoio/x-fidelity/compare/v3.5.0...v3.6.0) (2025-02-27)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * add type annotations to fix TypeScript errors in globalFileAnalysis ([588c814](https://github.com/zotoio/x-fidelity/commit/588c8140a348a4959e62c027bdadbd625a11dc51))
7
+
8
+
9
+ ### Features
10
+
11
+ * add global file analysis with pattern matching and ratio operators ([ab456df](https://github.com/zotoio/x-fidelity/commit/ab456df857efa82fd4f394f474b0b6f85b1fc91e))
12
+ * add support for new and legacy pattern analysis ([6702ea0](https://github.com/zotoio/x-fidelity/commit/6702ea081cff3fbc2abceed35e4aee073ff02b56))
13
+ * **targeting:** facts and operators to support rules applied to specific codebase matches ([231cb9a](https://github.com/zotoio/x-fidelity/commit/231cb9adedac8dc18666058925af835b68fe60e3))
14
+
1
15
  # [3.5.0](https://github.com/zotoio/x-fidelity/compare/v3.4.0...v3.5.0) (2025-02-27)
2
16
 
3
17
 
@@ -9,7 +9,8 @@
9
9
  "openaiAnalysisA11y-global",
10
10
  "invalidSystemIdConfigured-iterative",
11
11
  "missingRequiredFiles-global",
12
- "factDoesNotAddResultToAlmanac-iterative"
12
+ "factDoesNotAddResultToAlmanac-iterative",
13
+ "apiUsageConsistency-global"
13
14
  ],
14
15
  "operators": [
15
16
  "fileContains",
@@ -18,14 +19,17 @@
18
19
  "openaiAnalysisHighSeverity",
19
20
  "invalidRemoteValidation",
20
21
  "missingRequiredFiles",
21
- "regexMatch"
22
+ "regexMatch",
23
+ "globalPatternRatio",
24
+ "globalPatternCount"
22
25
  ],
23
26
  "facts": [
24
27
  "repoFilesystemFacts",
25
28
  "repoDependencyFacts",
26
29
  "openaiAnalysisFacts",
27
30
  "remoteSubstringValidation",
28
- "missingRequiredFiles"
31
+ "missingRequiredFiles",
32
+ "globalFileAnalysisFacts"
29
33
  ],
30
34
  "plugins": [
31
35
  "xfiPluginRequiredFiles",
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "apiUsageConsistency-global",
3
+ "conditions": {
4
+ "all": [
5
+ {
6
+ "fact": "fileData",
7
+ "path": "$.fileName",
8
+ "operator": "equal",
9
+ "value": "REPO_GLOBAL_CHECK"
10
+ },
11
+ {
12
+ "fact": "globalFileAnalysis",
13
+ "params": {
14
+ "newPatterns": [
15
+ "logger\\.info\\("
16
+ ],
17
+ "legacyPatterns": [
18
+ "logger\\.debug\\("
19
+ ],
20
+ "fileFilter": ".*\\.(ts|js)$",
21
+ "resultFact": "apiUsageAnalysis"
22
+ },
23
+ "operator": "globalPatternRatio",
24
+ "value": 0.3
25
+ }
26
+ ]
27
+ },
28
+ "event": {
29
+ "type": "warning",
30
+ "params": {
31
+ "message": "The codebase is not consistently using the new API methods. At least 80% of API calls should use the new methods.",
32
+ "details": {
33
+ "fact": "apiUsageAnalysis"
34
+ }
35
+ }
36
+ }
37
+ }
@@ -0,0 +1,2 @@
1
+ import { FactDefn } from '../types/typeDefs';
2
+ export declare const globalFileAnalysis: FactDefn;
@@ -0,0 +1,108 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.globalFileAnalysis = void 0;
13
+ const logger_1 = require("../utils/logger");
14
+ const maskSensitiveData_1 = require("../utils/maskSensitiveData");
15
+ exports.globalFileAnalysis = {
16
+ name: 'globalFileAnalysis',
17
+ fn: (params, almanac) => __awaiter(void 0, void 0, void 0, function* () {
18
+ const result = { result: [], matchCounts: {}, fileMatches: {} };
19
+ try {
20
+ // Get all file data from the almanac
21
+ const globalFileMetadata = yield almanac.factValue('globalFileMetadata');
22
+ if (!Array.isArray(globalFileMetadata)) {
23
+ logger_1.logger.error('Invalid globalFileMetadata');
24
+ return result;
25
+ }
26
+ // Extract parameters
27
+ const newPatterns = Array.isArray(params.newPatterns) ? params.newPatterns : (params.newPatterns ? [params.newPatterns] : []);
28
+ const legacyPatterns = Array.isArray(params.legacyPatterns) ? params.legacyPatterns : (params.legacyPatterns ? [params.legacyPatterns] : []);
29
+ // For backward compatibility
30
+ const patterns = Array.isArray(params.patterns) ? params.patterns : (params.patterns ? [params.patterns] : []);
31
+ const fileFilter = params.fileFilter || '.*';
32
+ const fileFilterRegex = new RegExp(fileFilter);
33
+ // Combine all patterns for processing
34
+ const allPatterns = [...newPatterns, ...legacyPatterns, ...patterns];
35
+ logger_1.logger.info(`Running global file analysis with ${newPatterns.length} new patterns, ${legacyPatterns.length} legacy patterns, and ${patterns.length} regular patterns across ${globalFileMetadata.length} files`);
36
+ // Filter files based on fileFilter parameter
37
+ const filteredFiles = globalFileMetadata.filter(file => file.fileName !== 'REPO_GLOBAL_CHECK' && fileFilterRegex.test(file.filePath));
38
+ logger_1.logger.info(`Analyzing ${filteredFiles.length} files after filtering`);
39
+ // Initialize match counts for each pattern
40
+ allPatterns.forEach((pattern) => {
41
+ result.matchCounts[pattern] = 0;
42
+ result.fileMatches[pattern] = [];
43
+ });
44
+ // Process each file
45
+ for (const file of filteredFiles) {
46
+ const fileContent = file.fileContent;
47
+ if (!fileContent)
48
+ continue;
49
+ const lines = fileContent.split('\n');
50
+ // Check each pattern against the file
51
+ for (const pattern of allPatterns) {
52
+ const regex = new RegExp(pattern, 'g');
53
+ let fileMatches = 0;
54
+ const matchDetails = [];
55
+ // Check each line for matches
56
+ for (let i = 0; i < lines.length; i++) {
57
+ const line = lines[i];
58
+ let match;
59
+ while ((match = regex.exec(line)) !== null) {
60
+ fileMatches++;
61
+ matchDetails.push({
62
+ lineNumber: i + 1,
63
+ match: match[0],
64
+ context: (0, maskSensitiveData_1.maskSensitiveData)(line.trim())
65
+ });
66
+ }
67
+ }
68
+ // Update results if matches found
69
+ if (fileMatches > 0) {
70
+ result.matchCounts[pattern] += fileMatches;
71
+ result.fileMatches[pattern].push({
72
+ filePath: file.filePath,
73
+ matchCount: fileMatches,
74
+ matches: matchDetails
75
+ });
76
+ }
77
+ }
78
+ }
79
+ // Calculate totals for new and legacy patterns
80
+ const newPatternsTotal = newPatterns.reduce((sum, pattern) => sum + (result.matchCounts[pattern] || 0), 0);
81
+ const legacyPatternsTotal = legacyPatterns.reduce((sum, pattern) => sum + (result.matchCounts[pattern] || 0), 0);
82
+ // Add summary to result
83
+ result.summary = {
84
+ totalFiles: filteredFiles.length,
85
+ totalMatches: Object.values(result.matchCounts).reduce((sum, count) => sum + count, 0),
86
+ patternCounts: result.matchCounts,
87
+ newPatternCounts: newPatterns.reduce((counts, pattern) => {
88
+ counts[pattern] = result.matchCounts[pattern] || 0;
89
+ return counts;
90
+ }, {}),
91
+ legacyPatternCounts: legacyPatterns.reduce((counts, pattern) => {
92
+ counts[pattern] = result.matchCounts[pattern] || 0;
93
+ return counts;
94
+ }, {}),
95
+ newPatternsTotal: newPatternsTotal,
96
+ legacyPatternsTotal: legacyPatternsTotal
97
+ };
98
+ // Add the result to the almanac
99
+ almanac.addRuntimeFact(params.resultFact, result);
100
+ logger_1.logger.info(`Global file analysis complete: ${JSON.stringify(result.summary)}`);
101
+ return result;
102
+ }
103
+ catch (error) {
104
+ logger_1.logger.error(`Error in globalFileAnalysis: ${error}`);
105
+ return result;
106
+ }
107
+ })
108
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,69 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ const globalFileAnalysisFacts_1 = require("./globalFileAnalysisFacts");
13
+ const logger_1 = require("../utils/logger");
14
+ jest.mock('../utils/logger', () => ({
15
+ logger: {
16
+ debug: jest.fn(),
17
+ error: jest.fn(),
18
+ info: jest.fn(),
19
+ warn: jest.fn(),
20
+ },
21
+ }));
22
+ describe('globalFileAnalysis', () => {
23
+ it('should analyze patterns across multiple files', () => __awaiter(void 0, void 0, void 0, function* () {
24
+ const mockAlmanac = {
25
+ factValue: jest.fn().mockResolvedValue([
26
+ {
27
+ fileName: 'file1.ts',
28
+ filePath: '/path/to/file1.ts',
29
+ fileContent: 'function test() { newApiMethod(); legacyApiMethod(); }'
30
+ },
31
+ {
32
+ fileName: 'file2.ts',
33
+ filePath: '/path/to/file2.ts',
34
+ fileContent: 'function test2() { newApiMethod(); newApiMethod(); }'
35
+ },
36
+ {
37
+ fileName: 'REPO_GLOBAL_CHECK',
38
+ filePath: 'REPO_GLOBAL_CHECK',
39
+ fileContent: 'REPO_GLOBAL_CHECK'
40
+ }
41
+ ]),
42
+ addRuntimeFact: jest.fn(),
43
+ };
44
+ const params = {
45
+ patterns: ['newApiMethod\\(', 'legacyApiMethod\\('],
46
+ fileFilter: '\\.ts$',
47
+ resultFact: 'apiUsageAnalysis'
48
+ };
49
+ const result = yield globalFileAnalysisFacts_1.globalFileAnalysis.fn(params, mockAlmanac);
50
+ expect(result.summary.totalFiles).toBe(2);
51
+ expect(result.matchCounts['newApiMethod\\(']).toBe(3);
52
+ expect(result.matchCounts['legacyApiMethod\\(']).toBe(1);
53
+ expect(mockAlmanac.addRuntimeFact).toHaveBeenCalledWith('apiUsageAnalysis', expect.any(Object));
54
+ }));
55
+ it('should handle errors gracefully', () => __awaiter(void 0, void 0, void 0, function* () {
56
+ const mockAlmanac = {
57
+ factValue: jest.fn().mockRejectedValue(new Error('Test error')),
58
+ addRuntimeFact: jest.fn(),
59
+ };
60
+ const params = {
61
+ patterns: ['newApiMethod\\('],
62
+ fileFilter: '\\.ts$',
63
+ resultFact: 'apiUsageAnalysis'
64
+ };
65
+ const result = yield globalFileAnalysisFacts_1.globalFileAnalysis.fn(params, mockAlmanac);
66
+ expect(result.result).toEqual([]);
67
+ expect(logger_1.logger.error).toHaveBeenCalled();
68
+ }));
69
+ });
@@ -13,6 +13,7 @@ exports.loadFacts = loadFacts;
13
13
  const repoFilesystemFacts_1 = require("./repoFilesystemFacts");
14
14
  const repoDependencyFacts_1 = require("./repoDependencyFacts");
15
15
  const openaiAnalysisFacts_1 = require("./openaiAnalysisFacts");
16
+ const globalFileAnalysisFacts_1 = require("./globalFileAnalysisFacts");
16
17
  const pluginRegistry_1 = require("../core/pluginRegistry");
17
18
  const openaiUtils_1 = require("../utils/openaiUtils");
18
19
  const logger_1 = require("../utils/logger");
@@ -29,6 +30,10 @@ function loadFacts(factNames) {
29
30
  }, openaiAnalysisFacts: {
30
31
  name: 'openaiAnalysis',
31
32
  fn: openaiAnalysisFacts_1.openaiAnalysis
33
+ }, globalFileAnalysisFacts: {
34
+ name: 'globalFileAnalysis',
35
+ fn: globalFileAnalysisFacts_1.globalFileAnalysis.fn,
36
+ priority: 50 // Set priority to run after globalFileMetadata
32
37
  } }, Object.fromEntries(pluginRegistry_1.pluginRegistry.getPluginFacts().map(fact => [fact.name, fact])));
33
38
  logger_1.logger.info(`Loading facts: ${factNames.join(', ')}`);
34
39
  const pluginFacts = pluginRegistry_1.pluginRegistry.getPluginFacts();
@@ -0,0 +1,3 @@
1
+ import { OperatorDefn } from '../types/typeDefs';
2
+ declare const globalPatternCount: OperatorDefn;
3
+ export { globalPatternCount };
@@ -0,0 +1,39 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.globalPatternCount = void 0;
4
+ const logger_1 = require("../utils/logger");
5
+ const globalPatternCount = {
6
+ 'name': 'globalPatternCount',
7
+ 'fn': (analysisResult, threshold) => {
8
+ try {
9
+ logger_1.logger.debug(`globalPatternCount: processing ${JSON.stringify(analysisResult)}`);
10
+ if (!analysisResult || !analysisResult.summary) {
11
+ logger_1.logger.debug('globalPatternCount: no analysis result available');
12
+ return false;
13
+ }
14
+ // Check if we have new pattern totals
15
+ if (analysisResult.summary.newPatternsTotal !== undefined) {
16
+ const newTotal = analysisResult.summary.newPatternsTotal;
17
+ logger_1.logger.info(`globalPatternCount: new patterns total: ${newTotal}, threshold: ${threshold}`);
18
+ // Compare count with threshold
19
+ return newTotal >= threshold;
20
+ }
21
+ // Fallback to original behavior for backward compatibility
22
+ const patterns = Object.keys(analysisResult.matchCounts);
23
+ if (patterns.length < 1) {
24
+ logger_1.logger.debug('globalPatternCount: no patterns found in analysis');
25
+ return false;
26
+ }
27
+ // Get count for the first pattern
28
+ const patternCount = analysisResult.matchCounts[patterns[0]] || 0;
29
+ logger_1.logger.info(`globalPatternCount: ${patternCount}, threshold: ${threshold}`);
30
+ // Compare count with threshold
31
+ return patternCount >= threshold;
32
+ }
33
+ catch (e) {
34
+ logger_1.logger.error(`globalPatternCount error: ${e}`);
35
+ return false;
36
+ }
37
+ }
38
+ };
39
+ exports.globalPatternCount = globalPatternCount;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const globalPatternCount_1 = require("./globalPatternCount");
4
+ jest.mock('../utils/logger', () => ({
5
+ logger: {
6
+ debug: jest.fn(),
7
+ error: jest.fn(),
8
+ info: jest.fn(),
9
+ },
10
+ }));
11
+ describe('globalPatternCount', () => {
12
+ it('should return true when count exceeds threshold', () => {
13
+ const analysisResult = {
14
+ matchCounts: {
15
+ 'pattern1': 10
16
+ },
17
+ summary: {
18
+ totalFiles: 5,
19
+ totalMatches: 10
20
+ }
21
+ };
22
+ expect(globalPatternCount_1.globalPatternCount.fn(analysisResult, 5)).toBe(true);
23
+ });
24
+ it('should return false when count is below threshold', () => {
25
+ const analysisResult = {
26
+ matchCounts: {
27
+ 'pattern1': 3
28
+ },
29
+ summary: {
30
+ totalFiles: 5,
31
+ totalMatches: 3
32
+ }
33
+ };
34
+ expect(globalPatternCount_1.globalPatternCount.fn(analysisResult, 5)).toBe(false);
35
+ });
36
+ it('should handle edge cases', () => {
37
+ // No patterns
38
+ expect(globalPatternCount_1.globalPatternCount.fn({ matchCounts: {}, summary: {} }, 1)).toBe(false);
39
+ // No matches
40
+ expect(globalPatternCount_1.globalPatternCount.fn({
41
+ matchCounts: { 'pattern1': 0 },
42
+ summary: { totalMatches: 0 }
43
+ }, 1)).toBe(false);
44
+ });
45
+ });
@@ -0,0 +1,3 @@
1
+ import { OperatorDefn } from '../types/typeDefs';
2
+ declare const globalPatternRatio: OperatorDefn;
3
+ export { globalPatternRatio };
@@ -0,0 +1,56 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.globalPatternRatio = void 0;
4
+ const logger_1 = require("../utils/logger");
5
+ const globalPatternRatio = {
6
+ 'name': 'globalPatternRatio',
7
+ 'fn': (analysisResult, threshold) => {
8
+ try {
9
+ logger_1.logger.debug(`globalPatternRatio: processing ${JSON.stringify(analysisResult)}`);
10
+ if (!analysisResult || !analysisResult.summary) {
11
+ logger_1.logger.debug('globalPatternRatio: no analysis result available');
12
+ return false;
13
+ }
14
+ // Check if we have new and legacy pattern totals
15
+ if (analysisResult.summary.newPatternsTotal !== undefined &&
16
+ analysisResult.summary.legacyPatternsTotal !== undefined) {
17
+ const newTotal = analysisResult.summary.newPatternsTotal;
18
+ const legacyTotal = analysisResult.summary.legacyPatternsTotal;
19
+ const total = newTotal + legacyTotal;
20
+ // Avoid division by zero
21
+ if (total === 0) {
22
+ logger_1.logger.debug('globalPatternRatio: no pattern matches found');
23
+ return false;
24
+ }
25
+ // Calculate ratio of new patterns to total patterns
26
+ const ratio = newTotal / total;
27
+ logger_1.logger.info(`globalPatternRatio: ${newTotal}/(${newTotal}+${legacyTotal}) = ${ratio}, threshold: ${threshold}`);
28
+ // Compare ratio with threshold
29
+ return ratio >= threshold;
30
+ }
31
+ // Fallback to original behavior for backward compatibility
32
+ const patterns = Object.keys(analysisResult.matchCounts);
33
+ if (patterns.length < 2) {
34
+ logger_1.logger.debug('globalPatternRatio: need at least 2 patterns to calculate ratio');
35
+ return false;
36
+ }
37
+ // Calculate ratio between first two patterns
38
+ const pattern1Count = analysisResult.matchCounts[patterns[0]] || 0;
39
+ const pattern2Count = analysisResult.matchCounts[patterns[1]] || 0;
40
+ // Avoid division by zero
41
+ if (pattern2Count === 0) {
42
+ logger_1.logger.debug('globalPatternRatio: denominator pattern has zero matches');
43
+ return false;
44
+ }
45
+ const ratio = pattern1Count / pattern2Count;
46
+ logger_1.logger.info(`globalPatternRatio: ${pattern1Count}/${pattern2Count} = ${ratio}, threshold: ${threshold}`);
47
+ // Compare ratio with threshold
48
+ return ratio >= threshold;
49
+ }
50
+ catch (e) {
51
+ logger_1.logger.error(`globalPatternRatio error: ${e}`);
52
+ return false;
53
+ }
54
+ }
55
+ };
56
+ exports.globalPatternRatio = globalPatternRatio;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,52 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const globalPatternRatio_1 = require("./globalPatternRatio");
4
+ jest.mock('../utils/logger', () => ({
5
+ logger: {
6
+ debug: jest.fn(),
7
+ error: jest.fn(),
8
+ info: jest.fn(),
9
+ },
10
+ }));
11
+ describe('globalPatternRatio', () => {
12
+ it('should return true when ratio exceeds threshold', () => {
13
+ const analysisResult = {
14
+ matchCounts: {
15
+ 'newApiMethod\\(': 8,
16
+ 'legacyApiMethod\\(': 2
17
+ },
18
+ summary: {
19
+ totalFiles: 5,
20
+ totalMatches: 10
21
+ }
22
+ };
23
+ expect(globalPatternRatio_1.globalPatternRatio.fn(analysisResult, 3)).toBe(true);
24
+ });
25
+ it('should return false when ratio is below threshold', () => {
26
+ const analysisResult = {
27
+ matchCounts: {
28
+ 'newApiMethod\\(': 4,
29
+ 'legacyApiMethod\\(': 6
30
+ },
31
+ summary: {
32
+ totalFiles: 5,
33
+ totalMatches: 10
34
+ }
35
+ };
36
+ expect(globalPatternRatio_1.globalPatternRatio.fn(analysisResult, 1)).toBe(false);
37
+ });
38
+ it('should handle edge cases', () => {
39
+ // No patterns
40
+ expect(globalPatternRatio_1.globalPatternRatio.fn({ matchCounts: {}, summary: {} }, 1)).toBe(false);
41
+ // Only one pattern
42
+ expect(globalPatternRatio_1.globalPatternRatio.fn({
43
+ matchCounts: { 'pattern1': 5 },
44
+ summary: { totalMatches: 5 }
45
+ }, 1)).toBe(false);
46
+ // Denominator is zero
47
+ expect(globalPatternRatio_1.globalPatternRatio.fn({
48
+ matchCounts: { 'pattern1': 5, 'pattern2': 0 },
49
+ summary: { totalMatches: 5 }
50
+ }, 1)).toBe(false);
51
+ });
52
+ });
@@ -15,6 +15,8 @@ const fileContains_1 = require("./fileContains");
15
15
  const nonStandardDirectoryStructure_1 = require("./nonStandardDirectoryStructure");
16
16
  const openaiAnalysisHighSeverity_1 = require("./openaiAnalysisHighSeverity");
17
17
  const regexMatch_1 = require("./regexMatch");
18
+ const globalPatternRatio_1 = require("./globalPatternRatio");
19
+ const globalPatternCount_1 = require("./globalPatternCount");
18
20
  const openaiUtils_1 = require("../utils/openaiUtils");
19
21
  const logger_1 = require("../utils/logger");
20
22
  const pluginRegistry_1 = require("../core/pluginRegistry");
@@ -25,7 +27,9 @@ function loadOperators(operatorNames) {
25
27
  fileContains: fileContains_1.fileContains,
26
28
  nonStandardDirectoryStructure: nonStandardDirectoryStructure_1.nonStandardDirectoryStructure,
27
29
  openaiAnalysisHighSeverity: openaiAnalysisHighSeverity_1.openaiAnalysisHighSeverity,
28
- regexMatch: regexMatch_1.regexMatch }, Object.fromEntries(pluginRegistry_1.pluginRegistry.getPluginOperators().map(op => [op.name, op])));
30
+ regexMatch: regexMatch_1.regexMatch,
31
+ globalPatternRatio: globalPatternRatio_1.globalPatternRatio,
32
+ globalPatternCount: globalPatternCount_1.globalPatternCount }, Object.fromEntries(pluginRegistry_1.pluginRegistry.getPluginOperators().map(op => [op.name, op])));
29
33
  const openAIStatus = (0, openaiUtils_1.getOpenAIStatus)();
30
34
  logger_1.logger.info(`Loading operators: ${operatorNames.join(', ')}`);
31
35
  const loadedOperators = [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "x-fidelity",
3
- "version": "3.5.0",
3
+ "version": "3.6.0",
4
4
  "description": "cli for opinionated framework adherence checks",
5
5
  "main": "dist/index",
6
6
  "types": "dist/index.d.ts",
@@ -9,7 +9,8 @@
9
9
  "openaiAnalysisA11y-global",
10
10
  "invalidSystemIdConfigured-iterative",
11
11
  "missingRequiredFiles-global",
12
- "factDoesNotAddResultToAlmanac-iterative"
12
+ "factDoesNotAddResultToAlmanac-iterative",
13
+ "apiUsageConsistency-global"
13
14
  ],
14
15
  "operators": [
15
16
  "fileContains",
@@ -18,14 +19,17 @@
18
19
  "openaiAnalysisHighSeverity",
19
20
  "invalidRemoteValidation",
20
21
  "missingRequiredFiles",
21
- "regexMatch"
22
+ "regexMatch",
23
+ "globalPatternRatio",
24
+ "globalPatternCount"
22
25
  ],
23
26
  "facts": [
24
27
  "repoFilesystemFacts",
25
28
  "repoDependencyFacts",
26
29
  "openaiAnalysisFacts",
27
30
  "remoteSubstringValidation",
28
- "missingRequiredFiles"
31
+ "missingRequiredFiles",
32
+ "globalFileAnalysisFacts"
29
33
  ],
30
34
  "plugins": [
31
35
  "xfiPluginRequiredFiles",
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "apiUsageConsistency-global",
3
+ "conditions": {
4
+ "all": [
5
+ {
6
+ "fact": "fileData",
7
+ "path": "$.fileName",
8
+ "operator": "equal",
9
+ "value": "REPO_GLOBAL_CHECK"
10
+ },
11
+ {
12
+ "fact": "globalFileAnalysis",
13
+ "params": {
14
+ "newPatterns": [
15
+ "logger\\.info\\("
16
+ ],
17
+ "legacyPatterns": [
18
+ "logger\\.debug\\("
19
+ ],
20
+ "fileFilter": ".*\\.(ts|js)$",
21
+ "resultFact": "apiUsageAnalysis"
22
+ },
23
+ "operator": "globalPatternRatio",
24
+ "value": 0.3
25
+ }
26
+ ]
27
+ },
28
+ "event": {
29
+ "type": "warning",
30
+ "params": {
31
+ "message": "The codebase is not consistently using the new API methods. At least 80% of API calls should use the new methods.",
32
+ "details": {
33
+ "fact": "apiUsageAnalysis"
34
+ }
35
+ }
36
+ }
37
+ }
@@ -0,0 +1,67 @@
1
+ import { globalFileAnalysis } from './globalFileAnalysisFacts';
2
+ import { logger } from '../utils/logger';
3
+
4
+ jest.mock('../utils/logger', () => ({
5
+ logger: {
6
+ debug: jest.fn(),
7
+ error: jest.fn(),
8
+ info: jest.fn(),
9
+ warn: jest.fn(),
10
+ },
11
+ }));
12
+
13
+ describe('globalFileAnalysis', () => {
14
+ it('should analyze patterns across multiple files', async () => {
15
+ const mockAlmanac = {
16
+ factValue: jest.fn().mockResolvedValue([
17
+ {
18
+ fileName: 'file1.ts',
19
+ filePath: '/path/to/file1.ts',
20
+ fileContent: 'function test() { newApiMethod(); legacyApiMethod(); }'
21
+ },
22
+ {
23
+ fileName: 'file2.ts',
24
+ filePath: '/path/to/file2.ts',
25
+ fileContent: 'function test2() { newApiMethod(); newApiMethod(); }'
26
+ },
27
+ {
28
+ fileName: 'REPO_GLOBAL_CHECK',
29
+ filePath: 'REPO_GLOBAL_CHECK',
30
+ fileContent: 'REPO_GLOBAL_CHECK'
31
+ }
32
+ ]),
33
+ addRuntimeFact: jest.fn(),
34
+ };
35
+
36
+ const params = {
37
+ patterns: ['newApiMethod\\(', 'legacyApiMethod\\('],
38
+ fileFilter: '\\.ts$',
39
+ resultFact: 'apiUsageAnalysis'
40
+ };
41
+
42
+ const result = await globalFileAnalysis.fn(params, mockAlmanac);
43
+
44
+ expect(result.summary.totalFiles).toBe(2);
45
+ expect(result.matchCounts['newApiMethod\\(']).toBe(3);
46
+ expect(result.matchCounts['legacyApiMethod\\(']).toBe(1);
47
+ expect(mockAlmanac.addRuntimeFact).toHaveBeenCalledWith('apiUsageAnalysis', expect.any(Object));
48
+ });
49
+
50
+ it('should handle errors gracefully', async () => {
51
+ const mockAlmanac = {
52
+ factValue: jest.fn().mockRejectedValue(new Error('Test error')),
53
+ addRuntimeFact: jest.fn(),
54
+ };
55
+
56
+ const params = {
57
+ patterns: ['newApiMethod\\('],
58
+ fileFilter: '\\.ts$',
59
+ resultFact: 'apiUsageAnalysis'
60
+ };
61
+
62
+ const result = await globalFileAnalysis.fn(params, mockAlmanac);
63
+
64
+ expect(result.result).toEqual([]);
65
+ expect(logger.error).toHaveBeenCalled();
66
+ });
67
+ });
@@ -0,0 +1,116 @@
1
+ import { logger } from '../utils/logger';
2
+ import { FactDefn, FileData } from '../types/typeDefs';
3
+ import { maskSensitiveData } from '../utils/maskSensitiveData';
4
+
5
+ export const globalFileAnalysis: FactDefn = {
6
+ name: 'globalFileAnalysis',
7
+ fn: async (params: any, almanac: any) => {
8
+ const result: any = { result: [], matchCounts: {}, fileMatches: {} };
9
+
10
+ try {
11
+ // Get all file data from the almanac
12
+ const globalFileMetadata: FileData[] = await almanac.factValue('globalFileMetadata');
13
+ if (!Array.isArray(globalFileMetadata)) {
14
+ logger.error('Invalid globalFileMetadata');
15
+ return result;
16
+ }
17
+
18
+ // Extract parameters
19
+ const newPatterns = Array.isArray(params.newPatterns) ? params.newPatterns : (params.newPatterns ? [params.newPatterns] : []);
20
+ const legacyPatterns = Array.isArray(params.legacyPatterns) ? params.legacyPatterns : (params.legacyPatterns ? [params.legacyPatterns] : []);
21
+ // For backward compatibility
22
+ const patterns = Array.isArray(params.patterns) ? params.patterns : (params.patterns ? [params.patterns] : []);
23
+
24
+ const fileFilter = params.fileFilter || '.*';
25
+ const fileFilterRegex = new RegExp(fileFilter);
26
+
27
+ // Combine all patterns for processing
28
+ const allPatterns = [...newPatterns, ...legacyPatterns, ...patterns];
29
+
30
+ logger.info(`Running global file analysis with ${newPatterns.length} new patterns, ${legacyPatterns.length} legacy patterns, and ${patterns.length} regular patterns across ${globalFileMetadata.length} files`);
31
+
32
+ // Filter files based on fileFilter parameter
33
+ const filteredFiles = globalFileMetadata.filter(file =>
34
+ file.fileName !== 'REPO_GLOBAL_CHECK' && fileFilterRegex.test(file.filePath)
35
+ );
36
+
37
+ logger.info(`Analyzing ${filteredFiles.length} files after filtering`);
38
+
39
+ // Initialize match counts for each pattern
40
+ allPatterns.forEach((pattern: string) => {
41
+ result.matchCounts[pattern] = 0;
42
+ result.fileMatches[pattern] = [];
43
+ });
44
+
45
+ // Process each file
46
+ for (const file of filteredFiles) {
47
+ const fileContent = file.fileContent;
48
+ if (!fileContent) continue;
49
+
50
+ const lines = fileContent.split('\n');
51
+
52
+ // Check each pattern against the file
53
+ for (const pattern of allPatterns) {
54
+ const regex = new RegExp(pattern, 'g');
55
+ let fileMatches = 0;
56
+ const matchDetails = [];
57
+
58
+ // Check each line for matches
59
+ for (let i = 0; i < lines.length; i++) {
60
+ const line = lines[i];
61
+ let match;
62
+ while ((match = regex.exec(line)) !== null) {
63
+ fileMatches++;
64
+ matchDetails.push({
65
+ lineNumber: i + 1,
66
+ match: match[0],
67
+ context: maskSensitiveData(line.trim())
68
+ });
69
+ }
70
+ }
71
+
72
+ // Update results if matches found
73
+ if (fileMatches > 0) {
74
+ result.matchCounts[pattern] += fileMatches;
75
+ result.fileMatches[pattern].push({
76
+ filePath: file.filePath,
77
+ matchCount: fileMatches,
78
+ matches: matchDetails
79
+ });
80
+ }
81
+ }
82
+ }
83
+
84
+ // Calculate totals for new and legacy patterns
85
+ const newPatternsTotal = newPatterns.reduce((sum: number, pattern: string) => sum + (result.matchCounts[pattern] || 0), 0);
86
+ const legacyPatternsTotal = legacyPatterns.reduce((sum: number, pattern: string) => sum + (result.matchCounts[pattern] || 0), 0);
87
+
88
+ // Add summary to result
89
+ result.summary = {
90
+ totalFiles: filteredFiles.length,
91
+ totalMatches: Object.values(result.matchCounts).reduce((sum: number, count: unknown) => sum + (count as number), 0),
92
+ patternCounts: result.matchCounts,
93
+ newPatternCounts: newPatterns.reduce((counts: Record<string, number>, pattern: string) => {
94
+ counts[pattern] = result.matchCounts[pattern] || 0;
95
+ return counts;
96
+ }, {}),
97
+ legacyPatternCounts: legacyPatterns.reduce((counts: Record<string, number>, pattern: string) => {
98
+ counts[pattern] = result.matchCounts[pattern] || 0;
99
+ return counts;
100
+ }, {}),
101
+ newPatternsTotal: newPatternsTotal,
102
+ legacyPatternsTotal: legacyPatternsTotal
103
+ };
104
+
105
+ // Add the result to the almanac
106
+ almanac.addRuntimeFact(params.resultFact, result);
107
+
108
+ logger.info(`Global file analysis complete: ${JSON.stringify(result.summary)}`);
109
+
110
+ return result;
111
+ } catch (error) {
112
+ logger.error(`Error in globalFileAnalysis: ${error}`);
113
+ return result;
114
+ }
115
+ }
116
+ };
@@ -1,6 +1,7 @@
1
1
  import { collectRepoFileData } from './repoFilesystemFacts';
2
2
  import { getDependencyVersionFacts } from './repoDependencyFacts';
3
3
  import { openaiAnalysis } from './openaiAnalysisFacts';
4
+ import { globalFileAnalysis } from './globalFileAnalysisFacts';
4
5
  import { pluginRegistry } from '../core/pluginRegistry';
5
6
  import { isOpenAIEnabled } from '../utils/openaiUtils';
6
7
  import { FactDefn } from '../types/typeDefs';
@@ -22,6 +23,11 @@ async function loadFacts(factNames: string[]): Promise<FactDefn[]> {
22
23
  name: 'openaiAnalysis',
23
24
  fn: openaiAnalysis
24
25
  },
26
+ globalFileAnalysisFacts: {
27
+ name: 'globalFileAnalysis',
28
+ fn: globalFileAnalysis.fn,
29
+ priority: 50 // Set priority to run after globalFileMetadata
30
+ },
25
31
  ...Object.fromEntries(
26
32
  pluginRegistry.getPluginFacts().map(fact => [fact.name, fact])
27
33
  )
@@ -0,0 +1,51 @@
1
+ import { globalPatternCount } from './globalPatternCount';
2
+ import { logger } from '../utils/logger';
3
+
4
+ jest.mock('../utils/logger', () => ({
5
+ logger: {
6
+ debug: jest.fn(),
7
+ error: jest.fn(),
8
+ info: jest.fn(),
9
+ },
10
+ }));
11
+
12
+ describe('globalPatternCount', () => {
13
+ it('should return true when count exceeds threshold', () => {
14
+ const analysisResult = {
15
+ matchCounts: {
16
+ 'pattern1': 10
17
+ },
18
+ summary: {
19
+ totalFiles: 5,
20
+ totalMatches: 10
21
+ }
22
+ };
23
+
24
+ expect(globalPatternCount.fn(analysisResult, 5)).toBe(true);
25
+ });
26
+
27
+ it('should return false when count is below threshold', () => {
28
+ const analysisResult = {
29
+ matchCounts: {
30
+ 'pattern1': 3
31
+ },
32
+ summary: {
33
+ totalFiles: 5,
34
+ totalMatches: 3
35
+ }
36
+ };
37
+
38
+ expect(globalPatternCount.fn(analysisResult, 5)).toBe(false);
39
+ });
40
+
41
+ it('should handle edge cases', () => {
42
+ // No patterns
43
+ expect(globalPatternCount.fn({ matchCounts: {}, summary: {} }, 1)).toBe(false);
44
+
45
+ // No matches
46
+ expect(globalPatternCount.fn({
47
+ matchCounts: { 'pattern1': 0 },
48
+ summary: { totalMatches: 0 }
49
+ }, 1)).toBe(false);
50
+ });
51
+ });
@@ -0,0 +1,44 @@
1
+ import { logger } from '../utils/logger';
2
+ import { OperatorDefn } from '../types/typeDefs';
3
+
4
+ const globalPatternCount: OperatorDefn = {
5
+ 'name': 'globalPatternCount',
6
+ 'fn': (analysisResult: any, threshold: number) => {
7
+ try {
8
+ logger.debug(`globalPatternCount: processing ${JSON.stringify(analysisResult)}`);
9
+
10
+ if (!analysisResult || !analysisResult.summary) {
11
+ logger.debug('globalPatternCount: no analysis result available');
12
+ return false;
13
+ }
14
+
15
+ // Check if we have new pattern totals
16
+ if (analysisResult.summary.newPatternsTotal !== undefined) {
17
+ const newTotal = analysisResult.summary.newPatternsTotal;
18
+ logger.info(`globalPatternCount: new patterns total: ${newTotal}, threshold: ${threshold}`);
19
+
20
+ // Compare count with threshold
21
+ return newTotal >= threshold;
22
+ }
23
+
24
+ // Fallback to original behavior for backward compatibility
25
+ const patterns = Object.keys(analysisResult.matchCounts);
26
+ if (patterns.length < 1) {
27
+ logger.debug('globalPatternCount: no patterns found in analysis');
28
+ return false;
29
+ }
30
+
31
+ // Get count for the first pattern
32
+ const patternCount = analysisResult.matchCounts[patterns[0]] || 0;
33
+ logger.info(`globalPatternCount: ${patternCount}, threshold: ${threshold}`);
34
+
35
+ // Compare count with threshold
36
+ return patternCount >= threshold;
37
+ } catch (e) {
38
+ logger.error(`globalPatternCount error: ${e}`);
39
+ return false;
40
+ }
41
+ }
42
+ };
43
+
44
+ export { globalPatternCount };
@@ -0,0 +1,59 @@
1
+ import { globalPatternRatio } from './globalPatternRatio';
2
+ import { logger } from '../utils/logger';
3
+
4
+ jest.mock('../utils/logger', () => ({
5
+ logger: {
6
+ debug: jest.fn(),
7
+ error: jest.fn(),
8
+ info: jest.fn(),
9
+ },
10
+ }));
11
+
12
+ describe('globalPatternRatio', () => {
13
+ it('should return true when ratio exceeds threshold', () => {
14
+ const analysisResult = {
15
+ matchCounts: {
16
+ 'newApiMethod\\(': 8,
17
+ 'legacyApiMethod\\(': 2
18
+ },
19
+ summary: {
20
+ totalFiles: 5,
21
+ totalMatches: 10
22
+ }
23
+ };
24
+
25
+ expect(globalPatternRatio.fn(analysisResult, 3)).toBe(true);
26
+ });
27
+
28
+ it('should return false when ratio is below threshold', () => {
29
+ const analysisResult = {
30
+ matchCounts: {
31
+ 'newApiMethod\\(': 4,
32
+ 'legacyApiMethod\\(': 6
33
+ },
34
+ summary: {
35
+ totalFiles: 5,
36
+ totalMatches: 10
37
+ }
38
+ };
39
+
40
+ expect(globalPatternRatio.fn(analysisResult, 1)).toBe(false);
41
+ });
42
+
43
+ it('should handle edge cases', () => {
44
+ // No patterns
45
+ expect(globalPatternRatio.fn({ matchCounts: {}, summary: {} }, 1)).toBe(false);
46
+
47
+ // Only one pattern
48
+ expect(globalPatternRatio.fn({
49
+ matchCounts: { 'pattern1': 5 },
50
+ summary: { totalMatches: 5 }
51
+ }, 1)).toBe(false);
52
+
53
+ // Denominator is zero
54
+ expect(globalPatternRatio.fn({
55
+ matchCounts: { 'pattern1': 5, 'pattern2': 0 },
56
+ summary: { totalMatches: 5 }
57
+ }, 1)).toBe(false);
58
+ });
59
+ });
@@ -0,0 +1,66 @@
1
+ import { logger } from '../utils/logger';
2
+ import { OperatorDefn } from '../types/typeDefs';
3
+
4
+ const globalPatternRatio: OperatorDefn = {
5
+ 'name': 'globalPatternRatio',
6
+ 'fn': (analysisResult: any, threshold: number) => {
7
+ try {
8
+ logger.debug(`globalPatternRatio: processing ${JSON.stringify(analysisResult)}`);
9
+
10
+ if (!analysisResult || !analysisResult.summary) {
11
+ logger.debug('globalPatternRatio: no analysis result available');
12
+ return false;
13
+ }
14
+
15
+ // Check if we have new and legacy pattern totals
16
+ if (analysisResult.summary.newPatternsTotal !== undefined &&
17
+ analysisResult.summary.legacyPatternsTotal !== undefined) {
18
+
19
+ const newTotal = analysisResult.summary.newPatternsTotal;
20
+ const legacyTotal = analysisResult.summary.legacyPatternsTotal;
21
+ const total = newTotal + legacyTotal;
22
+
23
+ // Avoid division by zero
24
+ if (total === 0) {
25
+ logger.debug('globalPatternRatio: no pattern matches found');
26
+ return false;
27
+ }
28
+
29
+ // Calculate ratio of new patterns to total patterns
30
+ const ratio = newTotal / total;
31
+ logger.info(`globalPatternRatio: ${newTotal}/(${newTotal}+${legacyTotal}) = ${ratio}, threshold: ${threshold}`);
32
+
33
+ // Compare ratio with threshold
34
+ return ratio >= threshold;
35
+ }
36
+
37
+ // Fallback to original behavior for backward compatibility
38
+ const patterns = Object.keys(analysisResult.matchCounts);
39
+ if (patterns.length < 2) {
40
+ logger.debug('globalPatternRatio: need at least 2 patterns to calculate ratio');
41
+ return false;
42
+ }
43
+
44
+ // Calculate ratio between first two patterns
45
+ const pattern1Count = analysisResult.matchCounts[patterns[0]] || 0;
46
+ const pattern2Count = analysisResult.matchCounts[patterns[1]] || 0;
47
+
48
+ // Avoid division by zero
49
+ if (pattern2Count === 0) {
50
+ logger.debug('globalPatternRatio: denominator pattern has zero matches');
51
+ return false;
52
+ }
53
+
54
+ const ratio = pattern1Count / pattern2Count;
55
+ logger.info(`globalPatternRatio: ${pattern1Count}/${pattern2Count} = ${ratio}, threshold: ${threshold}`);
56
+
57
+ // Compare ratio with threshold
58
+ return ratio >= threshold;
59
+ } catch (e) {
60
+ logger.error(`globalPatternRatio error: ${e}`);
61
+ return false;
62
+ }
63
+ }
64
+ };
65
+
66
+ export { globalPatternRatio };
@@ -4,6 +4,8 @@ import { fileContains } from './fileContains';
4
4
  import { nonStandardDirectoryStructure } from './nonStandardDirectoryStructure';
5
5
  import { openaiAnalysisHighSeverity } from './openaiAnalysisHighSeverity';
6
6
  import { regexMatch } from './regexMatch';
7
+ import { globalPatternRatio } from './globalPatternRatio';
8
+ import { globalPatternCount } from './globalPatternCount';
7
9
  import { getOpenAIStatus } from '../utils/openaiUtils';
8
10
  import { logger } from '../utils/logger';
9
11
  import { pluginRegistry } from '../core/pluginRegistry';
@@ -16,6 +18,8 @@ async function loadOperators(operatorNames: string[]): Promise<OperatorDefn[]> {
16
18
  nonStandardDirectoryStructure,
17
19
  openaiAnalysisHighSeverity,
18
20
  regexMatch,
21
+ globalPatternRatio,
22
+ globalPatternCount,
19
23
  ...Object.fromEntries(
20
24
  pluginRegistry.getPluginOperators().map(op => [op.name, op])
21
25
  )