x-fidelity 3.4.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.
Files changed (47) hide show
  1. package/.xfi-config.json +1 -1
  2. package/CHANGELOG.md +26 -0
  3. package/README.md +18 -1
  4. package/dist/core/configManager.js +13 -5
  5. package/dist/demoConfig/node-fullstack.json +11 -3
  6. package/dist/demoConfig/rules/apiUsageConsistency-global-rule.json +37 -0
  7. package/dist/demoConfig/rules/factDoesNotAddResultToAlmanac-iterative-rule.json +33 -0
  8. package/dist/facts/globalFileAnalysisFacts.d.ts +2 -0
  9. package/dist/facts/globalFileAnalysisFacts.js +108 -0
  10. package/dist/facts/globalFileAnalysisFacts.test.d.ts +1 -0
  11. package/dist/facts/globalFileAnalysisFacts.test.js +69 -0
  12. package/dist/facts/index.js +5 -0
  13. package/dist/facts/repoFilesystemFacts.js +1 -0
  14. package/dist/operators/globalPatternCount.d.ts +3 -0
  15. package/dist/operators/globalPatternCount.js +39 -0
  16. package/dist/operators/globalPatternCount.test.d.ts +1 -0
  17. package/dist/operators/globalPatternCount.test.js +45 -0
  18. package/dist/operators/globalPatternRatio.d.ts +3 -0
  19. package/dist/operators/globalPatternRatio.js +56 -0
  20. package/dist/operators/globalPatternRatio.test.d.ts +1 -0
  21. package/dist/operators/globalPatternRatio.test.js +52 -0
  22. package/dist/operators/index.js +5 -1
  23. package/dist/types/typeDefs.d.ts +1 -0
  24. package/dist/utils/jsonSchemas.js +1 -0
  25. package/package.json +1 -1
  26. package/src/core/configManager.ts +15 -5
  27. package/src/demoConfig/node-fullstack.json +11 -3
  28. package/src/demoConfig/rules/apiUsageConsistency-global-rule.json +37 -0
  29. package/src/demoConfig/rules/factDoesNotAddResultToAlmanac-iterative-rule.json +33 -0
  30. package/src/facts/globalFileAnalysisFacts.test.ts +67 -0
  31. package/src/facts/globalFileAnalysisFacts.ts +116 -0
  32. package/src/facts/index.ts +6 -0
  33. package/src/facts/repoFilesystemFacts.ts +1 -0
  34. package/src/operators/globalPatternCount.test.ts +51 -0
  35. package/src/operators/globalPatternCount.ts +44 -0
  36. package/src/operators/globalPatternRatio.test.ts +59 -0
  37. package/src/operators/globalPatternRatio.ts +66 -0
  38. package/src/operators/index.ts +4 -0
  39. package/src/types/typeDefs.ts +1 -0
  40. package/src/utils/jsonSchemas.ts +1 -0
  41. package/website/docs/archetypes.md +5 -0
  42. package/website/docs/ci-cd/overview.md +12 -0
  43. package/website/docs/getting-started.md +4 -0
  44. package/website/docs/key-concepts.md +1 -0
  45. package/website/docs/plugins/overview.md +25 -4
  46. package/dist/demoConfig/rules/regexMatch-example-iterative-rule.json +0 -28
  47. package/src/demoConfig/rules/regexMatch-example-iterative-rule.json +0 -28
package/.xfi-config.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "sensitiveFileFalsePositives": [
3
- "/README2.md"
3
+ "/src/facts/repoFilesystemFacts.ts"
4
4
  ]
5
5
  }
package/CHANGELOG.md CHANGED
@@ -1,3 +1,29 @@
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
+
15
+ # [3.5.0](https://github.com/zotoio/x-fidelity/compare/v3.4.0...v3.5.0) (2025-02-27)
16
+
17
+
18
+ ### Bug Fixes
19
+
20
+ * **regexmatch:** give better examples of iterative and negative matches ([3a4f211](https://github.com/zotoio/x-fidelity/commit/3a4f21157119a0677b29ebc211716526d8a762d0))
21
+
22
+
23
+ ### Features
24
+
25
+ * add plugin support to archetype configuration ([9e0a02b](https://github.com/zotoio/x-fidelity/commit/9e0a02be24fb812ca056157a0c3f1c7b8699b7f2))
26
+
1
27
  # [3.4.0](https://github.com/zotoio/x-fidelity/compare/v3.3.1...v3.4.0) (2025-02-26)
2
28
 
3
29
 
package/README.md CHANGED
@@ -179,6 +179,7 @@ x-fidelity is designed to be highly extensible, and only has demo rules and arch
179
179
  2. **Custom Rules**: Add new JSON rule files in the `rules` subdirectory of your local config or on your config server.
180
180
  3. **Custom Operators**: TODO: Implement new operators and add them to your x-fidelity fork or plugin.
181
181
  4. **Custom Facts**: TODO: Create new fact providers and add them to your x-fidelity fork or plugin.
182
+ 5. **Plugins**: Specify plugins in your archetype configuration or via the CLI to extend functionality.
182
183
 
183
184
  > At minimum you should configure your archetypes and rules using the provided facts and operators. Facts and Operators are more complex and are not yet easily added without forking, and those provided are intended to be flexible enough for general codebase analysis.
184
185
 
@@ -188,6 +189,7 @@ Archetypes represent a pattern or template of git repo that you expect to be con
188
189
  - **rules**: names of rules you have defined to perform either global (repo wide) checks, or checks on each file in a repo.
189
190
  - **facts**: data prepared before analysing a repo such as dependency data and file structures.
190
191
  - **operators**: these are custom operators, or provided operators that compare facts and return a boolean result. In x-fi boolean true means a rule failure.
192
+ - **plugins**: names of plugins to load when using this archetype. These extend the functionality with custom facts and operators.
191
193
  - **config**: core configuration related to codebase analysis:
192
194
  - minimumDependencyVersions: packages you want to conform to provided semver ranges.
193
195
  - standardStructure: expected filesystem directories at high level
@@ -201,6 +203,7 @@ Example of a custom archetype JSON file (`my-custom-archetype.json`):
201
203
  "rules": ["myCustomRule-global", "standardRule1-iterative", "standardRule2-iterative"],
202
204
  "operators": ["myCustomOperator", "standardOperator1"],
203
205
  "facts": ["myCustomFact", "standardFact1"],
206
+ "plugins": ["xfiPluginRequiredFiles", "myCustomPlugin"],
204
207
  "config": {
205
208
  "minimumDependencyVersions": {
206
209
  "my-framework": ">2.0.0"
@@ -836,7 +839,10 @@ New extensions are available:
836
839
  - *Remote String Validation Plugin:* (module: `xfiPluginRemoteStringValidator`) adds remote validation functionality via the `remoteSubstringValidation` fact and the `invalidRemoteValidation` operator.
837
840
  - *Sample Custom Plugin:* (module: `xfiPluginSimpleExample`) shows how to add custom facts and operators.
838
841
 
839
- To load these plugins, pass their module names via the `-e` (or `--extensions`) CLI option.
842
+ There are two ways to load plugins:
843
+
844
+ 1. **Via CLI option**: Pass plugin module names via the `-e` (or `--extensions`) CLI option.
845
+ 2. **Via archetype configuration**: Specify plugins in the `plugins` array of your archetype JSON file.
840
846
 
841
847
  x-fidelity supports custom extensions through npm modules. To use extensions:
842
848
 
@@ -852,6 +858,17 @@ xfidelity -e xfi-basic-plugin xfi-another-plugin
852
858
 
853
859
  Multiple extensions can be specified by separating them with spaces.
854
860
 
861
+ Alternatively, specify plugins in your archetype configuration:
862
+ ```json
863
+ {
864
+ "name": "my-archetype",
865
+ "plugins": ["xfi-basic-plugin", "xfi-another-plugin"],
866
+ // other archetype properties...
867
+ }
868
+ ```
869
+
870
+ Plugins specified in both the CLI and the archetype will be loaded, with CLI-specified plugins loaded first.
871
+
855
872
  ### Creating Extensions
856
873
 
857
874
  You can create custom extensions by implementing the XFiPlugin interface:
@@ -139,11 +139,6 @@ class ConfigManager {
139
139
  const localConfigPath = cli_1.options.localConfigPath;
140
140
  if (logPrefix)
141
141
  (0, logger_1.setLogPrefix)(logPrefix);
142
- // Load plugins during initialization
143
- yield this.loadPlugins(cli_1.options.extensions).catch(error => {
144
- logger_1.logger.error(error, `Error loading plugins`);
145
- throw error;
146
- });
147
142
  logger_1.logger.info(`Initializing config manager for archetype: ${archetype}`);
148
143
  logger_1.logger.debug(`Initialize params: ${JSON.stringify(params)}`);
149
144
  const config = {
@@ -162,6 +157,19 @@ class ConfigManager {
162
157
  if (!config.archetype || Object.keys(config.archetype).length === 0) {
163
158
  throw new Error(`No valid configuration found for archetype: ${archetype}`);
164
159
  }
160
+ // Load CLI-specified plugins first
161
+ yield this.loadPlugins(cli_1.options.extensions).catch(error => {
162
+ logger_1.logger.error(error, `Error loading CLI-specified plugins`);
163
+ throw error;
164
+ });
165
+ // Load archetype-specified plugins
166
+ if (config.archetype.plugins && config.archetype.plugins.length > 0) {
167
+ logger_1.logger.info(`Loading plugins specified by archetype: ${config.archetype.plugins.join(', ')}`);
168
+ yield this.loadPlugins(config.archetype.plugins).catch(error => {
169
+ logger_1.logger.error(error, `Error loading archetype-specified plugins`);
170
+ throw error;
171
+ });
172
+ }
165
173
  // Load all RuleConfig for the archetype
166
174
  config.rules = yield (0, ruleUtils_1.loadRules)({
167
175
  archetype,
@@ -9,7 +9,8 @@
9
9
  "openaiAnalysisA11y-global",
10
10
  "invalidSystemIdConfigured-iterative",
11
11
  "missingRequiredFiles-global",
12
- "regexMatch-example-iterative"
12
+ "factDoesNotAddResultToAlmanac-iterative",
13
+ "apiUsageConsistency-global"
13
14
  ],
14
15
  "operators": [
15
16
  "fileContains",
@@ -18,14 +19,21 @@
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"
33
+ ],
34
+ "plugins": [
35
+ "xfiPluginRequiredFiles",
36
+ "xfiPluginRemoteStringValidator"
29
37
  ],
30
38
  "config": {
31
39
  "minimumDependencyVersions": {
@@ -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,33 @@
1
+ {
2
+ "name": "factDoesNotAddResultToAlmanac-iterative",
3
+ "conditions": {
4
+ "all": [
5
+ {
6
+ "fact": "fileData",
7
+ "path": "$.filePath",
8
+ "operator": "regexMatch",
9
+ "value": "^.*\\/facts\\/(?!.*\\.test).*\\.ts$"
10
+ },
11
+ {
12
+ "not": {
13
+ "fact": "repoFileAnalysis",
14
+ "params": {
15
+ "checkPattern": [
16
+ "almanac\\.addRuntimeFact\\(params\\.resultFact"
17
+ ],
18
+ "resultFact": "factAlmanacResults"
19
+ },
20
+ "operator": "fileContains",
21
+ "value": true
22
+ }
23
+ }
24
+ ]
25
+ },
26
+ "event": {
27
+ "type": "warning",
28
+ "params": {
29
+ "message": "xfi facts should always add results to the almanac so that details can be reported.",
30
+ "details": "eg. almanac.addRuntimeFact(params.resultFact, result)"
31
+ }
32
+ }
33
+ }
@@ -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();
@@ -188,5 +188,6 @@ function repoFileAnalysis(params, almanac) {
188
188
  almanac.addRuntimeFact(params.resultFact, result);
189
189
  return result;
190
190
  // testing match on 'oracle'
191
+ // testing match on 'declare'
191
192
  });
192
193
  }
@@ -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
+ });