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