@rigour-labs/core 3.0.5 → 3.0.6

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/dist/gates/deprecated-apis-rules-lang.d.ts +21 -0
  2. package/dist/gates/deprecated-apis-rules-lang.js +311 -0
  3. package/dist/gates/deprecated-apis-rules-node.d.ts +19 -0
  4. package/dist/gates/deprecated-apis-rules-node.js +199 -0
  5. package/dist/gates/deprecated-apis-rules.d.ts +6 -0
  6. package/dist/gates/deprecated-apis-rules.js +6 -0
  7. package/dist/gates/deprecated-apis.js +1 -502
  8. package/dist/gates/hallucinated-imports-lang.d.ts +16 -0
  9. package/dist/gates/hallucinated-imports-lang.js +374 -0
  10. package/dist/gates/hallucinated-imports-stdlib.d.ts +12 -0
  11. package/dist/gates/hallucinated-imports-stdlib.js +228 -0
  12. package/dist/gates/hallucinated-imports.d.ts +0 -98
  13. package/dist/gates/hallucinated-imports.js +10 -678
  14. package/dist/gates/phantom-apis-data.d.ts +33 -0
  15. package/dist/gates/phantom-apis-data.js +398 -0
  16. package/dist/gates/phantom-apis.js +1 -393
  17. package/dist/gates/phantom-apis.test.js +52 -0
  18. package/dist/gates/promise-safety-helpers.d.ts +19 -0
  19. package/dist/gates/promise-safety-helpers.js +101 -0
  20. package/dist/gates/promise-safety-rules.d.ts +7 -0
  21. package/dist/gates/promise-safety-rules.js +19 -0
  22. package/dist/gates/promise-safety.d.ts +1 -21
  23. package/dist/gates/promise-safety.js +51 -257
  24. package/dist/gates/test-quality-lang.d.ts +30 -0
  25. package/dist/gates/test-quality-lang.js +188 -0
  26. package/dist/gates/test-quality.d.ts +0 -14
  27. package/dist/gates/test-quality.js +13 -186
  28. package/dist/pattern-index/indexer-helpers.d.ts +38 -0
  29. package/dist/pattern-index/indexer-helpers.js +111 -0
  30. package/dist/pattern-index/indexer-lang.d.ts +13 -0
  31. package/dist/pattern-index/indexer-lang.js +244 -0
  32. package/dist/pattern-index/indexer-ts.d.ts +22 -0
  33. package/dist/pattern-index/indexer-ts.js +258 -0
  34. package/dist/pattern-index/indexer.d.ts +4 -106
  35. package/dist/pattern-index/indexer.js +58 -707
  36. package/dist/pattern-index/staleness-data.d.ts +6 -0
  37. package/dist/pattern-index/staleness-data.js +262 -0
  38. package/dist/pattern-index/staleness.js +1 -258
  39. package/dist/templates/index.d.ts +12 -16
  40. package/dist/templates/index.js +11 -527
  41. package/dist/templates/paradigms.d.ts +2 -0
  42. package/dist/templates/paradigms.js +46 -0
  43. package/dist/templates/presets.d.ts +14 -0
  44. package/dist/templates/presets.js +227 -0
  45. package/dist/templates/universal-config.d.ts +2 -0
  46. package/dist/templates/universal-config.js +171 -0
  47. package/package.json +1 -1
@@ -3,45 +3,41 @@
3
3
  *
4
4
  * Scans the codebase and extracts patterns using AST parsing.
5
5
  * This is the core engine of the Pattern Index system.
6
+ *
7
+ * Language-specific extractors live in indexer-lang.ts.
8
+ * TypeScript/JS AST helpers live in indexer-ts.ts.
9
+ * Pure utility functions live in indexer-helpers.ts.
6
10
  */
7
11
  import * as fs from 'fs/promises';
8
12
  import * as path from 'path';
9
- import { createHash } from 'crypto';
10
13
  import { globby } from 'globby';
11
14
  import ts from 'typescript';
12
15
  import { generateEmbedding } from './embeddings.js';
13
- /** Default configuration for the indexer */
16
+ import { hashContent } from './indexer-helpers.js';
17
+ import { extractGoPatterns, extractRustPatterns, extractJVMStylePatterns, extractPythonPatterns, extractGenericCPatterns, } from './indexer-lang.js';
18
+ import { nodeToPattern } from './indexer-ts.js';
19
+ // ---------------------------------------------------------------------------
20
+ // Configuration
21
+ // ---------------------------------------------------------------------------
14
22
  const DEFAULT_CONFIG = {
15
23
  include: ['src/**/*', 'lib/**/*', 'app/**/*', 'components/**/*', 'utils/**/*', 'hooks/**/*', '**/tests/**/*', '**/test/**/*'],
16
24
  exclude: [
17
- '**/node_modules/**',
18
- '**/dist/**',
19
- '**/build/**',
20
- '**/.git/**',
21
- '**/coverage/**',
22
- '**/venv/**',
23
- '**/.venv/**',
24
- '**/__pycache__/**',
25
- '**/site-packages/**',
26
- '**/.pytest_cache/**',
27
- '**/target/**', // Rust build dir
28
- '**/bin/**',
29
- '**/.gradle/**',
30
- '**/.mvn/**'
25
+ '**/node_modules/**', '**/dist/**', '**/build/**', '**/.git/**',
26
+ '**/coverage/**', '**/venv/**', '**/.venv/**', '**/__pycache__/**',
27
+ '**/site-packages/**', '**/.pytest_cache/**', '**/target/**',
28
+ '**/bin/**', '**/.gradle/**', '**/.mvn/**',
31
29
  ],
32
30
  extensions: ['.ts', '.tsx', '.js', '.jsx', '.py', '.go', '.rs', '.java', '.cpp', '.h', '.rb', '.php', '.cs', '.kt'],
33
31
  indexTests: false,
34
32
  indexNodeModules: false,
35
33
  minNameLength: 2,
36
34
  categories: {},
37
- useEmbeddings: false
35
+ useEmbeddings: false,
38
36
  };
39
- /** Current index format version */
40
37
  const INDEX_VERSION = '1.0.0';
41
- /**
42
- * Pattern Indexer class.
43
- * Responsible for scanning and indexing code patterns.
44
- */
38
+ // ---------------------------------------------------------------------------
39
+ // PatternIndexer class
40
+ // ---------------------------------------------------------------------------
45
41
  export class PatternIndexer {
46
42
  config;
47
43
  rootDir;
@@ -51,9 +47,7 @@ export class PatternIndexer {
51
47
  }
52
48
  async buildIndex() {
53
49
  const startTime = Date.now();
54
- // Find all files to index
55
50
  const files = await this.findFiles();
56
- // Process files in parallel batches (concurrency: 10)
57
51
  const BATCH_SIZE = 10;
58
52
  const patterns = [];
59
53
  const indexedFiles = [];
@@ -63,7 +57,7 @@ export class PatternIndexer {
63
57
  try {
64
58
  const relativePath = path.relative(this.rootDir, file);
65
59
  const content = await fs.readFile(file, 'utf-8');
66
- const fileHash = this.hashContent(content);
60
+ const fileHash = hashContent(content);
67
61
  const filePatterns = await this.extractPatterns(file, content);
68
62
  return {
69
63
  patterns: filePatterns,
@@ -71,8 +65,8 @@ export class PatternIndexer {
71
65
  path: relativePath,
72
66
  hash: fileHash,
73
67
  patternCount: filePatterns.length,
74
- indexedAt: new Date().toISOString()
75
- }
68
+ indexedAt: new Date().toISOString(),
69
+ },
76
70
  };
77
71
  }
78
72
  catch (error) {
@@ -87,9 +81,7 @@ export class PatternIndexer {
87
81
  }
88
82
  }
89
83
  }
90
- // Generate embeddings in parallel batches if enabled
91
84
  if (this.config.useEmbeddings && patterns.length > 0) {
92
- // Use stderr to avoid contaminating JSON output on stdout
93
85
  process.stderr.write(`Generating embeddings for ${patterns.length} patterns...\n`);
94
86
  for (let i = 0; i < patterns.length; i += BATCH_SIZE) {
95
87
  const batch = patterns.slice(i, i + BATCH_SIZE);
@@ -100,60 +92,49 @@ export class PatternIndexer {
100
92
  }
101
93
  const endTime = Date.now();
102
94
  const stats = this.calculateStats(patterns, indexedFiles, endTime - startTime);
103
- const index = {
95
+ return {
104
96
  version: INDEX_VERSION,
105
97
  lastUpdated: new Date().toISOString(),
106
98
  rootDir: this.rootDir,
107
99
  patterns,
108
100
  stats,
109
- files: indexedFiles
101
+ files: indexedFiles,
110
102
  };
111
- return index;
112
103
  }
113
- /**
114
- * Incremental index update - only reindex changed files.
115
- */
116
104
  async updateIndex(existingIndex) {
117
105
  const startTime = Date.now();
118
106
  const files = await this.findFiles();
119
107
  const updatedPatterns = [];
120
108
  const updatedFiles = [];
121
- // Create a map of existing file hashes
122
109
  const existingFileMap = new Map(existingIndex.files.map(f => [f.path, f]));
123
- // Process files in parallel batches (concurrency: 10)
124
110
  const BATCH_SIZE = 10;
125
111
  for (let i = 0; i < files.length; i += BATCH_SIZE) {
126
112
  const batch = files.slice(i, i + BATCH_SIZE);
127
113
  const results = await Promise.all(batch.map(async (file) => {
128
114
  const relativePath = path.relative(this.rootDir, file);
129
115
  const content = await fs.readFile(file, 'utf-8');
130
- const fileHash = this.hashContent(content);
116
+ const fileHash = hashContent(content);
131
117
  const existingFile = existingFileMap.get(relativePath);
132
118
  if (existingFile && existingFile.hash === fileHash) {
133
- // File unchanged, keep existing patterns
134
119
  const existingPatterns = existingIndex.patterns.filter(p => p.file === relativePath);
135
120
  return { patterns: existingPatterns, fileInfo: existingFile };
136
121
  }
137
- else {
138
- // File changed or new, reindex
139
- const filePatterns = await this.extractPatterns(file, content);
140
- return {
141
- patterns: filePatterns,
142
- fileInfo: {
143
- path: relativePath,
144
- hash: fileHash,
145
- patternCount: filePatterns.length,
146
- indexedAt: new Date().toISOString()
147
- }
148
- };
149
- }
122
+ const filePatterns = await this.extractPatterns(file, content);
123
+ return {
124
+ patterns: filePatterns,
125
+ fileInfo: {
126
+ path: relativePath,
127
+ hash: fileHash,
128
+ patternCount: filePatterns.length,
129
+ indexedAt: new Date().toISOString(),
130
+ },
131
+ };
150
132
  }));
151
133
  for (const result of results) {
152
134
  updatedPatterns.push(...result.patterns);
153
135
  updatedFiles.push(result.fileInfo);
154
136
  }
155
137
  }
156
- // Update embeddings for new/changed patterns if enabled
157
138
  if (this.config.useEmbeddings && updatedPatterns.length > 0) {
158
139
  for (let i = 0; i < updatedPatterns.length; i += BATCH_SIZE) {
159
140
  const batch = updatedPatterns.slice(i, i + BATCH_SIZE);
@@ -172,48 +153,44 @@ export class PatternIndexer {
172
153
  rootDir: this.rootDir,
173
154
  patterns: updatedPatterns,
174
155
  stats,
175
- files: updatedFiles
156
+ files: updatedFiles,
176
157
  };
177
158
  }
178
- /**
179
- * Find all files to index based on configuration.
180
- */
159
+ // -------------------------------------------------------------------------
160
+ // Private helpers
161
+ // -------------------------------------------------------------------------
181
162
  async findFiles() {
182
- const patterns = this.config.include.map(p => this.config.extensions.map(ext => p.endsWith('*') ? `${p}${ext}` : p)).flat();
183
- let exclude = [...this.config.exclude];
163
+ const patterns = this.config.include
164
+ .map(p => this.config.extensions.map(ext => (p.endsWith('*') ? `${p}${ext}` : p)))
165
+ .flat();
166
+ const exclude = [...this.config.exclude];
184
167
  if (!this.config.indexTests) {
185
168
  exclude.push('**/*.test.*', '**/*.spec.*', '**/__tests__/**');
186
169
  }
187
- const files = await globby(patterns, {
170
+ return globby(patterns, {
188
171
  cwd: this.rootDir,
189
172
  absolute: true,
190
173
  ignore: exclude,
191
- gitignore: true
174
+ gitignore: true,
192
175
  });
193
- return files;
194
176
  }
195
- /**
196
- * Extract patterns from a single file using TypeScript AST.
197
- */
198
177
  async extractPatterns(filePath, content) {
199
178
  const ext = path.extname(filePath).toLowerCase();
200
- // Specific high-fidelity extractors
179
+ const { rootDir, config } = this;
201
180
  if (ext === '.py')
202
- return this.extractPythonPatterns(filePath, content);
181
+ return extractPythonPatterns(filePath, content, rootDir, config.minNameLength);
203
182
  if (ext === '.go')
204
- return this.extractGoPatterns(filePath, content);
183
+ return extractGoPatterns(filePath, content, rootDir);
205
184
  if (ext === '.rs')
206
- return this.extractRustPatterns(filePath, content);
185
+ return extractRustPatterns(filePath, content, rootDir);
207
186
  if (ext === '.java' || ext === '.kt' || ext === '.cs')
208
- return this.extractJVMStylePatterns(filePath, content);
209
- // Fallback for TS/JS or other C-style languages
210
- const patterns = [];
211
- const relativePath = path.relative(this.rootDir, filePath);
212
- // For TS/JS, use AST
187
+ return extractJVMStylePatterns(filePath, content, rootDir);
213
188
  if (['.ts', '.tsx', '.js', '.jsx'].includes(ext)) {
189
+ const patterns = [];
190
+ const relativePath = path.relative(this.rootDir, filePath);
214
191
  const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true, this.getScriptKind(filePath));
215
192
  const visit = (node) => {
216
- const pattern = this.nodeToPattern(node, sourceFile, relativePath, content);
193
+ const pattern = nodeToPattern(node, sourceFile, relativePath, content, config.minNameLength);
217
194
  if (pattern)
218
195
  patterns.push(pattern);
219
196
  ts.forEachChild(node, visit);
@@ -221,621 +198,10 @@ export class PatternIndexer {
221
198
  visit(sourceFile);
222
199
  return patterns;
223
200
  }
224
- // Generic C-style fallback (C++, PHP, etc.)
225
- return this.extractGenericCPatterns(filePath, content);
226
- }
227
- /**
228
- * Extract patterns from Go files.
229
- */
230
- extractGoPatterns(filePath, content) {
231
- const patterns = [];
232
- const relativePath = path.relative(this.rootDir, filePath);
233
- const lines = content.split('\n');
234
- const funcRegex = /^func\s+(?:\([^)]*\)\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*\(([^)]*)\)\s*([^\{]*)\s*\{/;
235
- const typeRegex = /^type\s+([A-Za-z_][A-Za-z0-9_]*)\s+(struct|interface)/;
236
- for (let i = 0; i < lines.length; i++) {
237
- const line = lines[i];
238
- // Functions
239
- const funcMatch = line.match(funcRegex);
240
- if (funcMatch) {
241
- const name = funcMatch[1];
242
- patterns.push(this.createPatternEntry({
243
- type: 'function',
244
- name,
245
- file: relativePath,
246
- line: i + 1,
247
- endLine: this.findBraceBlockEnd(lines, i),
248
- signature: `func ${name}(${funcMatch[2]}) ${funcMatch[3].trim()}`,
249
- description: this.getCOMLineComments(lines, i - 1),
250
- keywords: this.extractKeywords(name),
251
- content: this.getBraceBlockContent(lines, i),
252
- exported: /^[A-Z]/.test(name)
253
- }));
254
- }
255
- // Types/Structs
256
- const typeMatch = line.match(typeRegex);
257
- if (typeMatch) {
258
- const name = typeMatch[1];
259
- patterns.push(this.createPatternEntry({
260
- type: typeMatch[2],
261
- name,
262
- file: relativePath,
263
- line: i + 1,
264
- endLine: this.findBraceBlockEnd(lines, i),
265
- signature: `type ${name} ${typeMatch[2]}`,
266
- description: this.getCOMLineComments(lines, i - 1),
267
- keywords: this.extractKeywords(name),
268
- content: this.getBraceBlockContent(lines, i),
269
- exported: /^[A-Z]/.test(name)
270
- }));
271
- }
272
- }
273
- return patterns;
274
- }
275
- /**
276
- * Extract patterns from Rust files.
277
- */
278
- extractRustPatterns(filePath, content) {
279
- const patterns = [];
280
- const relativePath = path.relative(this.rootDir, filePath);
281
- const lines = content.split('\n');
282
- const fnRegex = /^(?:pub\s+)?(?:async\s+)?fn\s+([A-Za-z_][A-Za-z0-9_]*)\s*[<(][^)]*[>)]\s*(?:->\s*[^\{]+)?\s*\{/;
283
- const typeRegex = /^(?:pub\s+)?(struct|enum|trait)\s+([A-Za-z_][A-Za-z0-9_]*)/;
284
- for (let i = 0; i < lines.length; i++) {
285
- const line = lines[i];
286
- const fnMatch = line.match(fnRegex);
287
- if (fnMatch) {
288
- const name = fnMatch[1];
289
- patterns.push(this.createPatternEntry({
290
- type: 'function',
291
- name,
292
- file: relativePath,
293
- line: i + 1,
294
- endLine: this.findBraceBlockEnd(lines, i),
295
- signature: line.split('{')[0].trim(),
296
- description: this.getCOMLineComments(lines, i - 1),
297
- keywords: this.extractKeywords(name),
298
- content: this.getBraceBlockContent(lines, i),
299
- exported: line.startsWith('pub')
300
- }));
301
- }
302
- }
303
- return patterns;
304
- }
305
- /**
306
- * Generic extraction for C-style languages (Java, C++, PHP, etc.)
307
- */
308
- extractJVMStylePatterns(filePath, content) {
309
- const patterns = [];
310
- const relativePath = path.relative(this.rootDir, filePath);
311
- const lines = content.split('\n');
312
- // Simplified for classes and methods
313
- const classRegex = /^(?:public|private|protected|internal)?\s*(?:static\s+)?(?:final\s+)?(?:class|interface|enum)\s+([A-Za-z0-9_]+)/;
314
- const methodRegex = /^(?:public|private|protected|internal)\s+(?:static\s+)?(?:async\s+)?(?:[A-Za-z0-9_<>\[\]]+\s+)([A-Za-z0-9_]+)\s*\(/;
315
- for (let i = 0; i < lines.length; i++) {
316
- const line = lines[i].trim();
317
- const classMatch = line.match(classRegex);
318
- if (classMatch) {
319
- patterns.push(this.createPatternEntry({
320
- type: 'class',
321
- name: classMatch[1],
322
- file: relativePath,
323
- line: i + 1,
324
- endLine: this.findBraceBlockEnd(lines, i),
325
- signature: line,
326
- description: this.getJavaDoc(lines, i - 1),
327
- keywords: this.extractKeywords(classMatch[1]),
328
- content: this.getBraceBlockContent(lines, i),
329
- exported: line.includes('public')
330
- }));
331
- }
332
- }
333
- return patterns;
334
- }
335
- extractGenericCPatterns(filePath, content) {
336
- // Fallback for everything else
337
- return [];
338
- }
339
- getCOMLineComments(lines, startIndex) {
340
- let comments = [];
341
- for (let i = startIndex; i >= 0; i--) {
342
- const line = lines[i].trim();
343
- if (line.startsWith('//'))
344
- comments.unshift(line.replace('//', '').trim());
345
- else
346
- break;
347
- }
348
- return comments.join(' ');
349
- }
350
- getJavaDoc(lines, startIndex) {
351
- let comments = [];
352
- let inDoc = false;
353
- for (let i = startIndex; i >= 0; i--) {
354
- const line = lines[i].trim();
355
- if (line.endsWith('*/'))
356
- inDoc = true;
357
- if (inDoc)
358
- comments.unshift(line.replace('/**', '').replace('*/', '').replace('*', '').trim());
359
- if (line.startsWith('/**'))
360
- break;
361
- }
362
- return comments.join(' ');
363
- }
364
- findBraceBlockEnd(lines, startIndex) {
365
- let braceCount = 0;
366
- let started = false;
367
- for (let i = startIndex; i < lines.length; i++) {
368
- const line = lines[i];
369
- if (line.includes('{')) {
370
- braceCount += (line.match(/\{/g) || []).length;
371
- started = true;
372
- }
373
- if (line.includes('}')) {
374
- braceCount -= (line.match(/\}/g) || []).length;
375
- }
376
- if (started && braceCount === 0)
377
- return i + 1;
378
- }
379
- return lines.length;
380
- }
381
- getBraceBlockContent(lines, startIndex) {
382
- const end = this.findBraceBlockEnd(lines, startIndex);
383
- return lines.slice(startIndex, end).join('\n');
384
- }
385
- /**
386
- * Extract patterns from Python files using regex.
387
- */
388
- extractPythonPatterns(filePath, content) {
389
- const patterns = [];
390
- const relativePath = path.relative(this.rootDir, filePath);
391
- const lines = content.split('\n');
392
- // Regex for Class definitions
393
- const classRegex = /^class\s+([A-Za-z_][A-Za-z0-9_]*)\s*(\([^)]*\))?\s*:/;
394
- // Regex for Function definitions (including async)
395
- const funcRegex = /^(?:async\s+)?def\s+([A-Za-z_][A-Za-z0-9_]*)\s*\(([^)]*)\)\s*(?:->\s*[^:]+)?\s*:/;
396
- // Regex for Constants (Top-level UPPER_CASE variables)
397
- const constRegex = /^([A-Z][A-Z0-9_]*)\s*=\s*(.+)$/;
398
- for (let i = 0; i < lines.length; i++) {
399
- const lineContent = lines[i].trim();
400
- const originalLine = lines[i];
401
- const lineNum = i + 1;
402
- // Classes
403
- const classMatch = originalLine.match(classRegex);
404
- if (classMatch) {
405
- const name = classMatch[1];
406
- if (name.length >= this.config.minNameLength) {
407
- patterns.push(this.createPatternEntry({
408
- type: this.detectPythonClassType(name),
409
- name,
410
- file: relativePath,
411
- line: lineNum,
412
- endLine: this.findPythonBlockEnd(lines, i),
413
- signature: `class ${name}${classMatch[2] || ''}`,
414
- description: this.getPythonDocstring(lines, i + 1),
415
- keywords: this.extractKeywords(name),
416
- content: this.getPythonBlockContent(lines, i),
417
- exported: !name.startsWith('_')
418
- }));
419
- continue;
420
- }
421
- }
422
- // Functions
423
- const funcMatch = originalLine.match(funcRegex);
424
- if (funcMatch) {
425
- const name = funcMatch[1];
426
- if (name.length >= this.config.minNameLength) {
427
- patterns.push(this.createPatternEntry({
428
- type: this.detectPythonFunctionType(name),
429
- name,
430
- file: relativePath,
431
- line: lineNum,
432
- endLine: this.findPythonBlockEnd(lines, i),
433
- signature: `def ${name}(${funcMatch[2]})`,
434
- description: this.getPythonDocstring(lines, i + 1),
435
- keywords: this.extractKeywords(name),
436
- content: this.getPythonBlockContent(lines, i),
437
- exported: !name.startsWith('_')
438
- }));
439
- continue;
440
- }
441
- }
442
- // Constants
443
- const constMatch = originalLine.match(constRegex);
444
- if (constMatch) {
445
- const name = constMatch[1];
446
- if (name.length >= this.config.minNameLength) {
447
- patterns.push(this.createPatternEntry({
448
- type: 'constant',
449
- name,
450
- file: relativePath,
451
- line: lineNum,
452
- endLine: lineNum,
453
- signature: `${name} = ...`,
454
- description: '',
455
- keywords: this.extractKeywords(name),
456
- content: originalLine,
457
- exported: !name.startsWith('_')
458
- }));
459
- }
460
- }
461
- }
462
- return patterns;
463
- }
464
- detectPythonClassType(name) {
465
- if (name.endsWith('Error') || name.endsWith('Exception'))
466
- return 'error';
467
- if (name.endsWith('Model'))
468
- return 'model';
469
- if (name.endsWith('Schema'))
470
- return 'schema';
471
- return 'class';
472
- }
473
- detectPythonFunctionType(name) {
474
- if (name.startsWith('test_'))
475
- return 'function'; // Tests are filtered by indexTests config
476
- if (name.includes('middleware'))
477
- return 'middleware';
478
- if (name.includes('handler'))
479
- return 'handler';
480
- return 'function';
481
- }
482
- getPythonDocstring(lines, startIndex) {
483
- if (startIndex >= lines.length)
484
- return '';
485
- const nextLine = lines[startIndex].trim();
486
- if (nextLine.startsWith('"""') || nextLine.startsWith("'''")) {
487
- const quote = nextLine.startsWith('"""') ? '"""' : "'''";
488
- let doc = nextLine.replace(quote, '');
489
- if (doc.endsWith(quote))
490
- return doc.replace(quote, '').trim();
491
- for (let i = startIndex + 1; i < lines.length; i++) {
492
- if (lines[i].includes(quote)) {
493
- doc += ' ' + lines[i].split(quote)[0].trim();
494
- break;
495
- }
496
- doc += ' ' + lines[i].trim();
497
- }
498
- return doc.trim();
499
- }
500
- return '';
201
+ return extractGenericCPatterns(filePath, content);
501
202
  }
502
- findPythonBlockEnd(lines, startIndex) {
503
- const startIndent = lines[startIndex].search(/\S/);
504
- for (let i = startIndex + 1; i < lines.length; i++) {
505
- if (lines[i].trim() === '')
506
- continue;
507
- const currentIndent = lines[i].search(/\S/);
508
- if (currentIndent <= startIndent)
509
- return i;
510
- }
511
- return lines.length;
512
- }
513
- getPythonBlockContent(lines, startIndex) {
514
- const endLine = this.findPythonBlockEnd(lines, startIndex);
515
- return lines.slice(startIndex, endLine).join('\n');
516
- }
517
- /**
518
- * Convert an AST node to a PatternEntry if applicable.
519
- */
520
- nodeToPattern(node, sourceFile, filePath, content) {
521
- const startPos = sourceFile.getLineAndCharacterOfPosition(node.getStart());
522
- const endPos = sourceFile.getLineAndCharacterOfPosition(node.getEnd());
523
- const line = startPos.line + 1;
524
- const endLine = endPos.line + 1;
525
- // Function declarations
526
- if (ts.isFunctionDeclaration(node) && node.name) {
527
- const name = node.name.text;
528
- if (name.length < this.config.minNameLength)
529
- return null;
530
- return this.createPatternEntry({
531
- type: this.detectFunctionType(name, node),
532
- name,
533
- file: filePath,
534
- line,
535
- endLine,
536
- signature: this.getFunctionSignature(node, sourceFile),
537
- description: this.getJSDocDescription(node, sourceFile),
538
- keywords: this.extractKeywords(name),
539
- content: node.getText(sourceFile),
540
- exported: this.isExported(node)
541
- });
542
- }
543
- // Variable declarations with arrow functions
544
- if (ts.isVariableStatement(node)) {
545
- const patterns = [];
546
- for (const decl of node.declarationList.declarations) {
547
- if (ts.isIdentifier(decl.name) && decl.initializer) {
548
- const name = decl.name.text;
549
- if (name.length < this.config.minNameLength)
550
- continue;
551
- if (ts.isArrowFunction(decl.initializer) || ts.isFunctionExpression(decl.initializer)) {
552
- return this.createPatternEntry({
553
- type: this.detectFunctionType(name, decl.initializer),
554
- name,
555
- file: filePath,
556
- line,
557
- endLine,
558
- signature: this.getArrowFunctionSignature(decl.initializer, sourceFile),
559
- description: this.getJSDocDescription(node, sourceFile),
560
- keywords: this.extractKeywords(name),
561
- content: node.getText(sourceFile),
562
- exported: this.isExported(node)
563
- });
564
- }
565
- // Constants
566
- if (ts.isStringLiteral(decl.initializer) ||
567
- ts.isNumericLiteral(decl.initializer) ||
568
- ts.isObjectLiteralExpression(decl.initializer)) {
569
- const isConstant = node.declarationList.flags & ts.NodeFlags.Const;
570
- if (isConstant && name === name.toUpperCase()) {
571
- return this.createPatternEntry({
572
- type: 'constant',
573
- name,
574
- file: filePath,
575
- line,
576
- endLine,
577
- signature: '',
578
- description: this.getJSDocDescription(node, sourceFile),
579
- keywords: this.extractKeywords(name),
580
- content: node.getText(sourceFile),
581
- exported: this.isExported(node)
582
- });
583
- }
584
- }
585
- }
586
- }
587
- }
588
- // Class declarations
589
- if (ts.isClassDeclaration(node) && node.name) {
590
- const name = node.name.text;
591
- if (name.length < this.config.minNameLength)
592
- return null;
593
- return this.createPatternEntry({
594
- type: this.detectClassType(name, node),
595
- name,
596
- file: filePath,
597
- line,
598
- endLine,
599
- signature: this.getClassSignature(node, sourceFile),
600
- description: this.getJSDocDescription(node, sourceFile),
601
- keywords: this.extractKeywords(name),
602
- content: node.getText(sourceFile),
603
- exported: this.isExported(node)
604
- });
605
- }
606
- // Interface declarations
607
- if (ts.isInterfaceDeclaration(node)) {
608
- const name = node.name.text;
609
- if (name.length < this.config.minNameLength)
610
- return null;
611
- return this.createPatternEntry({
612
- type: 'interface',
613
- name,
614
- file: filePath,
615
- line,
616
- endLine,
617
- signature: this.getInterfaceSignature(node, sourceFile),
618
- description: this.getJSDocDescription(node, sourceFile),
619
- keywords: this.extractKeywords(name),
620
- content: node.getText(sourceFile),
621
- exported: this.isExported(node)
622
- });
623
- }
624
- // Type alias declarations
625
- if (ts.isTypeAliasDeclaration(node)) {
626
- const name = node.name.text;
627
- if (name.length < this.config.minNameLength)
628
- return null;
629
- return this.createPatternEntry({
630
- type: 'type',
631
- name,
632
- file: filePath,
633
- line,
634
- endLine,
635
- signature: node.getText(sourceFile).split('=')[0].trim(),
636
- description: this.getJSDocDescription(node, sourceFile),
637
- keywords: this.extractKeywords(name),
638
- content: node.getText(sourceFile),
639
- exported: this.isExported(node)
640
- });
641
- }
642
- // Enum declarations
643
- if (ts.isEnumDeclaration(node)) {
644
- const name = node.name.text;
645
- if (name.length < this.config.minNameLength)
646
- return null;
647
- return this.createPatternEntry({
648
- type: 'enum',
649
- name,
650
- file: filePath,
651
- line,
652
- endLine,
653
- signature: `enum ${name}`,
654
- description: this.getJSDocDescription(node, sourceFile),
655
- keywords: this.extractKeywords(name),
656
- content: node.getText(sourceFile),
657
- exported: this.isExported(node)
658
- });
659
- }
660
- return null;
661
- }
662
- /**
663
- * Detect the specific type of a function based on naming conventions.
664
- */
665
- detectFunctionType(name, node) {
666
- // React hooks
667
- if (name.startsWith('use') && name.length > 3 && name[3] === name[3].toUpperCase()) {
668
- return 'hook';
669
- }
670
- // React components (PascalCase and returns JSX)
671
- if (name[0] === name[0].toUpperCase() && this.containsJSX(node)) {
672
- return 'component';
673
- }
674
- // Middleware patterns
675
- if (name.includes('Middleware') || name.includes('middleware')) {
676
- return 'middleware';
677
- }
678
- // Handler patterns
679
- if (name.includes('Handler') || name.includes('handler')) {
680
- return 'handler';
681
- }
682
- // Factory patterns
683
- if (name.startsWith('create') || name.startsWith('make') || name.includes('Factory')) {
684
- return 'factory';
685
- }
686
- return 'function';
687
- }
688
- /**
689
- * Detect the specific type of a class.
690
- */
691
- detectClassType(name, node) {
692
- // Error classes
693
- if (name.endsWith('Error') || name.endsWith('Exception')) {
694
- return 'error';
695
- }
696
- // Check for React component (extends Component/PureComponent)
697
- if (node.heritageClauses) {
698
- for (const clause of node.heritageClauses) {
699
- const text = clause.getText();
700
- if (text.includes('Component') || text.includes('PureComponent')) {
701
- return 'component';
702
- }
703
- }
704
- }
705
- // Store patterns
706
- if (name.endsWith('Store') || name.endsWith('State')) {
707
- return 'store';
708
- }
709
- // Model patterns
710
- if (name.endsWith('Model') || name.endsWith('Entity')) {
711
- return 'model';
712
- }
713
- return 'class';
714
- }
715
- /**
716
- * Check if a node contains JSX.
717
- */
718
- containsJSX(node) {
719
- let hasJSX = false;
720
- const visit = (n) => {
721
- if (ts.isJsxElement(n) || ts.isJsxSelfClosingElement(n) || ts.isJsxFragment(n)) {
722
- hasJSX = true;
723
- return;
724
- }
725
- ts.forEachChild(n, visit);
726
- };
727
- visit(node);
728
- return hasJSX;
729
- }
730
- /**
731
- * Get function signature.
732
- */
733
- getFunctionSignature(node, sourceFile) {
734
- const params = node.parameters
735
- .map(p => p.getText(sourceFile))
736
- .join(', ');
737
- const returnType = node.type ? `: ${node.type.getText(sourceFile)}` : '';
738
- return `(${params})${returnType}`;
739
- }
740
- /**
741
- * Get arrow function signature.
742
- */
743
- getArrowFunctionSignature(node, sourceFile) {
744
- const params = node.parameters
745
- .map(p => p.getText(sourceFile))
746
- .join(', ');
747
- const returnType = node.type ? `: ${node.type.getText(sourceFile)}` : '';
748
- return `(${params})${returnType}`;
749
- }
750
- /**
751
- * Get class signature.
752
- */
753
- getClassSignature(node, sourceFile) {
754
- let sig = `class ${node.name?.text || 'Anonymous'}`;
755
- if (node.heritageClauses) {
756
- sig += ' ' + node.heritageClauses.map(c => c.getText(sourceFile)).join(' ');
757
- }
758
- return sig;
759
- }
760
- /**
761
- * Get interface signature.
762
- */
763
- getInterfaceSignature(node, sourceFile) {
764
- let sig = `interface ${node.name.text}`;
765
- if (node.typeParameters) {
766
- sig += `<${node.typeParameters.map(p => p.getText(sourceFile)).join(', ')}>`;
767
- }
768
- return sig;
769
- }
770
- /**
771
- * Extract JSDoc description from a node.
772
- */
773
- getJSDocDescription(node, sourceFile) {
774
- const jsDocTags = ts.getJSDocTags(node);
775
- const jsDocComment = ts.getJSDocCommentsAndTags(node);
776
- for (const tag of jsDocComment) {
777
- if (ts.isJSDoc(tag) && tag.comment) {
778
- if (typeof tag.comment === 'string') {
779
- return tag.comment;
780
- }
781
- return tag.comment.map(c => c.getText(sourceFile)).join(' ');
782
- }
783
- }
784
- return '';
785
- }
786
- /**
787
- * Check if a node is exported.
788
- */
789
- isExported(node) {
790
- if (ts.canHaveModifiers(node)) {
791
- const modifiers = ts.getModifiers(node);
792
- if (modifiers) {
793
- return modifiers.some(m => m.kind === ts.SyntaxKind.ExportKeyword);
794
- }
795
- }
796
- return false;
797
- }
798
- /**
799
- * Extract keywords from a name for semantic matching.
800
- */
801
- extractKeywords(name) {
802
- // Split camelCase and PascalCase
803
- const words = name
804
- .replace(/([a-z])([A-Z])/g, '$1 $2')
805
- .replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2')
806
- .toLowerCase()
807
- .split(/[\s_-]+/)
808
- .filter(w => w.length > 1);
809
- return [...new Set(words)];
810
- }
811
- /**
812
- * Create a PatternEntry with computed fields.
813
- */
814
- createPatternEntry(params) {
815
- const id = this.hashContent(`${params.file}:${params.name}:${params.line}`);
816
- const hash = this.hashContent(params.content);
817
- return {
818
- id,
819
- type: params.type,
820
- name: params.name,
821
- file: params.file,
822
- line: params.line,
823
- endLine: params.endLine,
824
- signature: params.signature,
825
- description: params.description,
826
- keywords: params.keywords,
827
- hash,
828
- exported: params.exported,
829
- usageCount: 0, // Will be calculated in a separate pass
830
- indexedAt: new Date().toISOString()
831
- };
832
- }
833
- /**
834
- * Get the TypeScript ScriptKind for a file.
835
- */
836
203
  getScriptKind(filePath) {
837
- const ext = path.extname(filePath).toLowerCase();
838
- switch (ext) {
204
+ switch (path.extname(filePath).toLowerCase()) {
839
205
  case '.ts': return ts.ScriptKind.TS;
840
206
  case '.tsx': return ts.ScriptKind.TSX;
841
207
  case '.js': return ts.ScriptKind.JS;
@@ -843,9 +209,6 @@ export class PatternIndexer {
843
209
  default: return ts.ScriptKind.TS;
844
210
  }
845
211
  }
846
- /**
847
- * Calculate index statistics.
848
- */
849
212
  calculateStats(patterns, files, durationMs) {
850
213
  const byType = {};
851
214
  for (const pattern of patterns) {
@@ -855,26 +218,17 @@ export class PatternIndexer {
855
218
  totalPatterns: patterns.length,
856
219
  totalFiles: files.length,
857
220
  byType: byType,
858
- indexDurationMs: durationMs
221
+ indexDurationMs: durationMs,
859
222
  };
860
223
  }
861
- /**
862
- * Hash content using SHA-256.
863
- */
864
- hashContent(content) {
865
- return createHash('sha256').update(content).digest('hex').slice(0, 16);
866
- }
867
224
  }
868
- /**
869
- * Save a pattern index to disk.
870
- */
225
+ // ---------------------------------------------------------------------------
226
+ // Standalone utilities (re-exported through pattern-index/index.ts)
227
+ // ---------------------------------------------------------------------------
871
228
  export async function savePatternIndex(index, outputPath) {
872
229
  await fs.mkdir(path.dirname(outputPath), { recursive: true });
873
230
  await fs.writeFile(outputPath, JSON.stringify(index, null, 2), 'utf-8');
874
231
  }
875
- /**
876
- * Load a pattern index from disk.
877
- */
878
232
  export async function loadPatternIndex(indexPath) {
879
233
  try {
880
234
  const content = await fs.readFile(indexPath, 'utf-8');
@@ -884,9 +238,6 @@ export async function loadPatternIndex(indexPath) {
884
238
  return null;
885
239
  }
886
240
  }
887
- /**
888
- * Get the default index path for a project.
889
- */
890
241
  export function getDefaultIndexPath(rootDir) {
891
242
  return path.join(rootDir, '.rigour', 'patterns.json');
892
243
  }