@rigour-labs/core 2.19.1 → 2.19.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.
@@ -72,4 +72,158 @@ describe('Context Awareness Engine', () => {
72
72
  const specificFailures = driftFailures.filter(f => f.files?.includes('valid.js'));
73
73
  expect(specificFailures.length).toBe(0);
74
74
  });
75
+ it('should classify arrow function exports as camelCase, not unknown', async () => {
76
+ // Create files with arrow function patterns that previously returned 'unknown'
77
+ await fs.writeFile(path.join(TEST_CWD, 'api.ts'), `
78
+ export const fetchData = async () => { return []; };
79
+ export const getUserProfile = async (id: string) => { return {}; };
80
+ export const use = () => {};
81
+ export const get = async () => {};
82
+ const handleClick = (e: Event) => {};
83
+ let processItem = async (item: any) => {};
84
+ `);
85
+ // Create a second file with consistent arrow function naming
86
+ await fs.writeFile(path.join(TEST_CWD, 'service.ts'), `
87
+ export const createUser = async (data: any) => {};
88
+ export const deleteUser = async (id: string) => {};
89
+ export const updateUser = async (id: string, data: any) => {};
90
+ `);
91
+ const config = {
92
+ version: 1,
93
+ commands: {},
94
+ gates: {
95
+ context: {
96
+ enabled: true,
97
+ sensitivity: 0.8,
98
+ mining_depth: 10,
99
+ ignored_patterns: [],
100
+ cross_file_patterns: true,
101
+ naming_consistency: true,
102
+ import_relationships: true,
103
+ max_cross_file_depth: 50,
104
+ },
105
+ },
106
+ output: { report_path: 'rigour-report.json' }
107
+ };
108
+ const runner = new GateRunner(config);
109
+ const report = await runner.run(TEST_CWD);
110
+ // Should NOT have any "unknown" naming convention failures
111
+ const namingFailures = report.failures.filter(f => f.id === 'context-drift' && f.details?.includes('unknown'));
112
+ expect(namingFailures.length).toBe(0);
113
+ });
114
+ it('should not classify plain variable declarations as function patterns', async () => {
115
+ // Create file with non-function const declarations
116
+ await fs.writeFile(path.join(TEST_CWD, 'constants.ts'), `
117
+ export const API_URL = 'https://api.example.com';
118
+ export const MAX_RETRIES = 3;
119
+ const config = { timeout: 5000 };
120
+ let count = 0;
121
+ `);
122
+ // Create file with actual functions for a dominant pattern
123
+ await fs.writeFile(path.join(TEST_CWD, 'utils.ts'), `
124
+ function getData() { return []; }
125
+ function setData(d: any) { return d; }
126
+ function processRequest(req: any) { return req; }
127
+ `);
128
+ const config = {
129
+ version: 1,
130
+ commands: {},
131
+ gates: {
132
+ context: {
133
+ enabled: true,
134
+ sensitivity: 0.8,
135
+ mining_depth: 10,
136
+ ignored_patterns: [],
137
+ cross_file_patterns: true,
138
+ naming_consistency: true,
139
+ import_relationships: true,
140
+ max_cross_file_depth: 50,
141
+ },
142
+ },
143
+ output: { report_path: 'rigour-report.json' }
144
+ };
145
+ const runner = new GateRunner(config);
146
+ const report = await runner.run(TEST_CWD);
147
+ // SCREAMING_SNAKE constants should NOT create naming drift failures
148
+ // because they should not be in the 'function' pattern bucket at all
149
+ const namingFailures = report.failures.filter(f => f.id === 'context-drift' && f.details?.includes('SCREAMING_SNAKE'));
150
+ expect(namingFailures.length).toBe(0);
151
+ });
152
+ });
153
+ /**
154
+ * Direct unit tests for detectCasing logic
155
+ */
156
+ describe('detectCasing classification', () => {
157
+ // We test the regex rules directly since detectCasing is private
158
+ function detectCasing(name) {
159
+ if (/^[A-Z][a-z]/.test(name) && /[a-z][A-Z]/.test(name))
160
+ return 'PascalCase';
161
+ if (/^[a-z]/.test(name) && /[a-z][A-Z]/.test(name))
162
+ return 'camelCase';
163
+ if (/^[a-z][a-zA-Z0-9]*$/.test(name))
164
+ return 'camelCase'; // single-word lowercase
165
+ if (/^[a-z]+(_[a-z]+)+$/.test(name))
166
+ return 'snake_case';
167
+ if (/^[A-Z]+(_[A-Z]+)*$/.test(name))
168
+ return 'SCREAMING_SNAKE';
169
+ if (/^[A-Z][a-zA-Z]*$/.test(name))
170
+ return 'PascalCase';
171
+ return 'unknown';
172
+ }
173
+ // Multi-word camelCase
174
+ it('classifies multi-word camelCase', () => {
175
+ expect(detectCasing('fetchData')).toBe('camelCase');
176
+ expect(detectCasing('getUserProfile')).toBe('camelCase');
177
+ expect(detectCasing('handleClick')).toBe('camelCase');
178
+ expect(detectCasing('processItem')).toBe('camelCase');
179
+ expect(detectCasing('createNewUser')).toBe('camelCase');
180
+ });
181
+ // Single-word lowercase (the bug fix)
182
+ it('classifies single-word lowercase as camelCase', () => {
183
+ expect(detectCasing('fetch')).toBe('camelCase');
184
+ expect(detectCasing('use')).toBe('camelCase');
185
+ expect(detectCasing('get')).toBe('camelCase');
186
+ expect(detectCasing('set')).toBe('camelCase');
187
+ expect(detectCasing('run')).toBe('camelCase');
188
+ expect(detectCasing('a')).toBe('camelCase');
189
+ expect(detectCasing('x')).toBe('camelCase');
190
+ expect(detectCasing('id')).toBe('camelCase');
191
+ });
192
+ // Single-word lowercase with digits
193
+ it('classifies lowercase with digits as camelCase', () => {
194
+ expect(detectCasing('handler2')).toBe('camelCase');
195
+ expect(detectCasing('config3')).toBe('camelCase');
196
+ expect(detectCasing('v2')).toBe('camelCase');
197
+ });
198
+ // PascalCase
199
+ it('classifies PascalCase', () => {
200
+ expect(detectCasing('MyComponent')).toBe('PascalCase');
201
+ expect(detectCasing('UserService')).toBe('PascalCase');
202
+ expect(detectCasing('App')).toBe('PascalCase');
203
+ expect(detectCasing('A')).toBe('SCREAMING_SNAKE'); // single uppercase letter
204
+ });
205
+ // snake_case
206
+ it('classifies snake_case', () => {
207
+ expect(detectCasing('my_func')).toBe('snake_case');
208
+ expect(detectCasing('get_data')).toBe('snake_case');
209
+ expect(detectCasing('process_all_items')).toBe('snake_case');
210
+ });
211
+ // SCREAMING_SNAKE
212
+ it('classifies SCREAMING_SNAKE_CASE', () => {
213
+ expect(detectCasing('API_URL')).toBe('SCREAMING_SNAKE');
214
+ expect(detectCasing('MAX_RETRIES')).toBe('SCREAMING_SNAKE');
215
+ expect(detectCasing('A')).toBe('SCREAMING_SNAKE');
216
+ expect(detectCasing('DB')).toBe('SCREAMING_SNAKE');
217
+ });
218
+ // Edge cases that should NOT be unknown
219
+ it('does not return unknown for valid identifiers', () => {
220
+ const validIdentifiers = [
221
+ 'fetch', 'getData', 'MyClass', 'my_func', 'API_KEY',
222
+ 'use', 'run', 'a', 'x', 'id', 'App', 'handler2',
223
+ 'processItem', 'UserProfile', 'get_all_data', 'MAX_SIZE',
224
+ ];
225
+ for (const name of validIdentifiers) {
226
+ expect(detectCasing(name)).not.toBe('unknown');
227
+ }
228
+ });
75
229
  });
@@ -70,11 +70,22 @@ export class ContextGate extends Gate {
70
70
  * Collect naming patterns (function names, class names, variable names)
71
71
  */
72
72
  collectNamingPatterns(content, file, patterns) {
73
- // Function declarations
74
- const funcMatches = content.matchAll(/(?:function|const|let|var)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*[=(]/g);
75
- for (const match of funcMatches) {
76
- const name = match[1];
77
- const casing = this.detectCasing(name);
73
+ // Named function declarations: function fetchData() { ... }
74
+ const namedFuncMatches = content.matchAll(/function\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\(/g);
75
+ for (const match of namedFuncMatches) {
76
+ const casing = this.detectCasing(match[1]);
77
+ this.addPattern(patterns, 'function', { casing, file, count: 1 });
78
+ }
79
+ // Arrow function expressions: (export) const fetchData = (async) (...) => { ... }
80
+ const arrowFuncMatches = content.matchAll(/(?:export\s+)?(?:const|let|var)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=\s*(?:async\s+)?(?:\([^)]*\)|[a-zA-Z_$][a-zA-Z0-9_$]*)\s*=>/g);
81
+ for (const match of arrowFuncMatches) {
82
+ const casing = this.detectCasing(match[1]);
83
+ this.addPattern(patterns, 'function', { casing, file, count: 1 });
84
+ }
85
+ // Function expressions: (export) const fetchData = (async) function(...) { ... }
86
+ const funcExprMatches = content.matchAll(/(?:export\s+)?(?:const|let|var)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=\s*(?:async\s+)?function\s*\(/g);
87
+ for (const match of funcExprMatches) {
88
+ const casing = this.detectCasing(match[1]);
78
89
  this.addPattern(patterns, 'function', { casing, file, count: 1 });
79
90
  }
80
91
  // Class declarations
@@ -175,6 +186,8 @@ export class ContextGate extends Gate {
175
186
  return 'PascalCase';
176
187
  if (/^[a-z]/.test(name) && /[a-z][A-Z]/.test(name))
177
188
  return 'camelCase';
189
+ if (/^[a-z][a-zA-Z0-9]*$/.test(name))
190
+ return 'camelCase'; // single-word lowercase (e.g. fetch, use, get)
178
191
  if (/^[a-z]+(_[a-z]+)+$/.test(name))
179
192
  return 'snake_case';
180
193
  if (/^[A-Z]+(_[A-Z]+)*$/.test(name))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rigour-labs/core",
3
- "version": "2.19.1",
3
+ "version": "2.19.2",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -84,4 +84,173 @@ describe('Context Awareness Engine', () => {
84
84
  const specificFailures = driftFailures.filter(f => f.files?.includes('valid.js'));
85
85
  expect(specificFailures.length).toBe(0);
86
86
  });
87
+ it('should classify arrow function exports as camelCase, not unknown', async () => {
88
+ // Create files with arrow function patterns that previously returned 'unknown'
89
+ await fs.writeFile(path.join(TEST_CWD, 'api.ts'), `
90
+ export const fetchData = async () => { return []; };
91
+ export const getUserProfile = async (id: string) => { return {}; };
92
+ export const use = () => {};
93
+ export const get = async () => {};
94
+ const handleClick = (e: Event) => {};
95
+ let processItem = async (item: any) => {};
96
+ `);
97
+
98
+ // Create a second file with consistent arrow function naming
99
+ await fs.writeFile(path.join(TEST_CWD, 'service.ts'), `
100
+ export const createUser = async (data: any) => {};
101
+ export const deleteUser = async (id: string) => {};
102
+ export const updateUser = async (id: string, data: any) => {};
103
+ `);
104
+
105
+ const config = {
106
+ version: 1,
107
+ commands: {},
108
+ gates: {
109
+ context: {
110
+ enabled: true,
111
+ sensitivity: 0.8,
112
+ mining_depth: 10,
113
+ ignored_patterns: [],
114
+ cross_file_patterns: true,
115
+ naming_consistency: true,
116
+ import_relationships: true,
117
+ max_cross_file_depth: 50,
118
+ },
119
+ },
120
+ output: { report_path: 'rigour-report.json' }
121
+ };
122
+
123
+ const runner = new GateRunner(config as any);
124
+ const report = await runner.run(TEST_CWD);
125
+
126
+ // Should NOT have any "unknown" naming convention failures
127
+ const namingFailures = report.failures.filter(f =>
128
+ f.id === 'context-drift' && f.details?.includes('unknown')
129
+ );
130
+ expect(namingFailures.length).toBe(0);
131
+ });
132
+
133
+ it('should not classify plain variable declarations as function patterns', async () => {
134
+ // Create file with non-function const declarations
135
+ await fs.writeFile(path.join(TEST_CWD, 'constants.ts'), `
136
+ export const API_URL = 'https://api.example.com';
137
+ export const MAX_RETRIES = 3;
138
+ const config = { timeout: 5000 };
139
+ let count = 0;
140
+ `);
141
+
142
+ // Create file with actual functions for a dominant pattern
143
+ await fs.writeFile(path.join(TEST_CWD, 'utils.ts'), `
144
+ function getData() { return []; }
145
+ function setData(d: any) { return d; }
146
+ function processRequest(req: any) { return req; }
147
+ `);
148
+
149
+ const config = {
150
+ version: 1,
151
+ commands: {},
152
+ gates: {
153
+ context: {
154
+ enabled: true,
155
+ sensitivity: 0.8,
156
+ mining_depth: 10,
157
+ ignored_patterns: [],
158
+ cross_file_patterns: true,
159
+ naming_consistency: true,
160
+ import_relationships: true,
161
+ max_cross_file_depth: 50,
162
+ },
163
+ },
164
+ output: { report_path: 'rigour-report.json' }
165
+ };
166
+
167
+ const runner = new GateRunner(config as any);
168
+ const report = await runner.run(TEST_CWD);
169
+
170
+ // SCREAMING_SNAKE constants should NOT create naming drift failures
171
+ // because they should not be in the 'function' pattern bucket at all
172
+ const namingFailures = report.failures.filter(f =>
173
+ f.id === 'context-drift' && f.details?.includes('SCREAMING_SNAKE')
174
+ );
175
+ expect(namingFailures.length).toBe(0);
176
+ });
177
+ });
178
+
179
+ /**
180
+ * Direct unit tests for detectCasing logic
181
+ */
182
+ describe('detectCasing classification', () => {
183
+ // We test the regex rules directly since detectCasing is private
184
+ function detectCasing(name: string): string {
185
+ if (/^[A-Z][a-z]/.test(name) && /[a-z][A-Z]/.test(name)) return 'PascalCase';
186
+ if (/^[a-z]/.test(name) && /[a-z][A-Z]/.test(name)) return 'camelCase';
187
+ if (/^[a-z][a-zA-Z0-9]*$/.test(name)) return 'camelCase'; // single-word lowercase
188
+ if (/^[a-z]+(_[a-z]+)+$/.test(name)) return 'snake_case';
189
+ if (/^[A-Z]+(_[A-Z]+)*$/.test(name)) return 'SCREAMING_SNAKE';
190
+ if (/^[A-Z][a-zA-Z]*$/.test(name)) return 'PascalCase';
191
+ return 'unknown';
192
+ }
193
+
194
+ // Multi-word camelCase
195
+ it('classifies multi-word camelCase', () => {
196
+ expect(detectCasing('fetchData')).toBe('camelCase');
197
+ expect(detectCasing('getUserProfile')).toBe('camelCase');
198
+ expect(detectCasing('handleClick')).toBe('camelCase');
199
+ expect(detectCasing('processItem')).toBe('camelCase');
200
+ expect(detectCasing('createNewUser')).toBe('camelCase');
201
+ });
202
+
203
+ // Single-word lowercase (the bug fix)
204
+ it('classifies single-word lowercase as camelCase', () => {
205
+ expect(detectCasing('fetch')).toBe('camelCase');
206
+ expect(detectCasing('use')).toBe('camelCase');
207
+ expect(detectCasing('get')).toBe('camelCase');
208
+ expect(detectCasing('set')).toBe('camelCase');
209
+ expect(detectCasing('run')).toBe('camelCase');
210
+ expect(detectCasing('a')).toBe('camelCase');
211
+ expect(detectCasing('x')).toBe('camelCase');
212
+ expect(detectCasing('id')).toBe('camelCase');
213
+ });
214
+
215
+ // Single-word lowercase with digits
216
+ it('classifies lowercase with digits as camelCase', () => {
217
+ expect(detectCasing('handler2')).toBe('camelCase');
218
+ expect(detectCasing('config3')).toBe('camelCase');
219
+ expect(detectCasing('v2')).toBe('camelCase');
220
+ });
221
+
222
+ // PascalCase
223
+ it('classifies PascalCase', () => {
224
+ expect(detectCasing('MyComponent')).toBe('PascalCase');
225
+ expect(detectCasing('UserService')).toBe('PascalCase');
226
+ expect(detectCasing('App')).toBe('PascalCase');
227
+ expect(detectCasing('A')).toBe('SCREAMING_SNAKE'); // single uppercase letter
228
+ });
229
+
230
+ // snake_case
231
+ it('classifies snake_case', () => {
232
+ expect(detectCasing('my_func')).toBe('snake_case');
233
+ expect(detectCasing('get_data')).toBe('snake_case');
234
+ expect(detectCasing('process_all_items')).toBe('snake_case');
235
+ });
236
+
237
+ // SCREAMING_SNAKE
238
+ it('classifies SCREAMING_SNAKE_CASE', () => {
239
+ expect(detectCasing('API_URL')).toBe('SCREAMING_SNAKE');
240
+ expect(detectCasing('MAX_RETRIES')).toBe('SCREAMING_SNAKE');
241
+ expect(detectCasing('A')).toBe('SCREAMING_SNAKE');
242
+ expect(detectCasing('DB')).toBe('SCREAMING_SNAKE');
243
+ });
244
+
245
+ // Edge cases that should NOT be unknown
246
+ it('does not return unknown for valid identifiers', () => {
247
+ const validIdentifiers = [
248
+ 'fetch', 'getData', 'MyClass', 'my_func', 'API_KEY',
249
+ 'use', 'run', 'a', 'x', 'id', 'App', 'handler2',
250
+ 'processItem', 'UserProfile', 'get_all_data', 'MAX_SIZE',
251
+ ];
252
+ for (const name of validIdentifiers) {
253
+ expect(detectCasing(name)).not.toBe('unknown');
254
+ }
255
+ });
87
256
  });
@@ -107,11 +107,24 @@ export class ContextGate extends Gate {
107
107
  file: string,
108
108
  patterns: Map<string, { casing: string; file: string; count: number }[]>
109
109
  ) {
110
- // Function declarations
111
- const funcMatches = content.matchAll(/(?:function|const|let|var)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*[=(]/g);
112
- for (const match of funcMatches) {
113
- const name = match[1];
114
- const casing = this.detectCasing(name);
110
+ // Named function declarations: function fetchData() { ... }
111
+ const namedFuncMatches = content.matchAll(/function\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\(/g);
112
+ for (const match of namedFuncMatches) {
113
+ const casing = this.detectCasing(match[1]);
114
+ this.addPattern(patterns, 'function', { casing, file, count: 1 });
115
+ }
116
+
117
+ // Arrow function expressions: (export) const fetchData = (async) (...) => { ... }
118
+ const arrowFuncMatches = content.matchAll(/(?:export\s+)?(?:const|let|var)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=\s*(?:async\s+)?(?:\([^)]*\)|[a-zA-Z_$][a-zA-Z0-9_$]*)\s*=>/g);
119
+ for (const match of arrowFuncMatches) {
120
+ const casing = this.detectCasing(match[1]);
121
+ this.addPattern(patterns, 'function', { casing, file, count: 1 });
122
+ }
123
+
124
+ // Function expressions: (export) const fetchData = (async) function(...) { ... }
125
+ const funcExprMatches = content.matchAll(/(?:export\s+)?(?:const|let|var)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=\s*(?:async\s+)?function\s*\(/g);
126
+ for (const match of funcExprMatches) {
127
+ const casing = this.detectCasing(match[1]);
115
128
  this.addPattern(patterns, 'function', { casing, file, count: 1 });
116
129
  }
117
130
 
@@ -234,6 +247,7 @@ export class ContextGate extends Gate {
234
247
  private detectCasing(name: string): string {
235
248
  if (/^[A-Z][a-z]/.test(name) && /[a-z][A-Z]/.test(name)) return 'PascalCase';
236
249
  if (/^[a-z]/.test(name) && /[a-z][A-Z]/.test(name)) return 'camelCase';
250
+ if (/^[a-z][a-zA-Z0-9]*$/.test(name)) return 'camelCase'; // single-word lowercase (e.g. fetch, use, get)
237
251
  if (/^[a-z]+(_[a-z]+)+$/.test(name)) return 'snake_case';
238
252
  if (/^[A-Z]+(_[A-Z]+)*$/.test(name)) return 'SCREAMING_SNAKE';
239
253
  if (/^[A-Z][a-zA-Z]*$/.test(name)) return 'PascalCase';