@rigour-labs/core 2.9.3 → 2.10.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 (38) hide show
  1. package/dist/index.js +4 -0
  2. package/dist/pattern-index/embeddings.d.ts +19 -0
  3. package/dist/pattern-index/embeddings.js +78 -0
  4. package/dist/pattern-index/index.d.ts +11 -0
  5. package/dist/pattern-index/index.js +15 -0
  6. package/dist/pattern-index/indexer.d.ts +101 -0
  7. package/dist/pattern-index/indexer.js +573 -0
  8. package/dist/pattern-index/indexer.test.d.ts +6 -0
  9. package/dist/pattern-index/indexer.test.js +188 -0
  10. package/dist/pattern-index/matcher.d.ts +106 -0
  11. package/dist/pattern-index/matcher.js +376 -0
  12. package/dist/pattern-index/matcher.test.d.ts +6 -0
  13. package/dist/pattern-index/matcher.test.js +238 -0
  14. package/dist/pattern-index/overrides.d.ts +64 -0
  15. package/dist/pattern-index/overrides.js +196 -0
  16. package/dist/pattern-index/security.d.ts +25 -0
  17. package/dist/pattern-index/security.js +127 -0
  18. package/dist/pattern-index/staleness.d.ts +71 -0
  19. package/dist/pattern-index/staleness.js +381 -0
  20. package/dist/pattern-index/staleness.test.d.ts +6 -0
  21. package/dist/pattern-index/staleness.test.js +211 -0
  22. package/dist/pattern-index/types.d.ts +221 -0
  23. package/dist/pattern-index/types.js +7 -0
  24. package/package.json +14 -1
  25. package/src/index.ts +4 -0
  26. package/src/pattern-index/embeddings.ts +84 -0
  27. package/src/pattern-index/index.ts +53 -0
  28. package/src/pattern-index/indexer.test.ts +276 -0
  29. package/src/pattern-index/indexer.ts +693 -0
  30. package/src/pattern-index/matcher.test.ts +293 -0
  31. package/src/pattern-index/matcher.ts +493 -0
  32. package/src/pattern-index/overrides.ts +235 -0
  33. package/src/pattern-index/security.ts +151 -0
  34. package/src/pattern-index/staleness.test.ts +313 -0
  35. package/src/pattern-index/staleness.ts +438 -0
  36. package/src/pattern-index/types.ts +339 -0
  37. package/vitest.config.ts +7 -0
  38. package/vitest.setup.ts +30 -0
@@ -0,0 +1,573 @@
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
+ import * as fs from 'fs/promises';
8
+ import * as path from 'path';
9
+ import { createHash } from 'crypto';
10
+ import { globby } from 'globby';
11
+ import ts from 'typescript';
12
+ import { generateEmbedding } from './embeddings.js';
13
+ /** Default configuration for the indexer */
14
+ const DEFAULT_CONFIG = {
15
+ include: ['src/**/*', 'lib/**/*', 'app/**/*', 'components/**/*', 'utils/**/*', 'hooks/**/*'],
16
+ exclude: ['**/node_modules/**', '**/dist/**', '**/build/**', '**/.git/**', '**/coverage/**'],
17
+ extensions: ['.ts', '.tsx', '.js', '.jsx'],
18
+ indexTests: false,
19
+ indexNodeModules: false,
20
+ minNameLength: 2,
21
+ categories: {},
22
+ useEmbeddings: false
23
+ };
24
+ /** Current index format version */
25
+ const INDEX_VERSION = '1.0.0';
26
+ /**
27
+ * Pattern Indexer class.
28
+ * Responsible for scanning and indexing code patterns.
29
+ */
30
+ export class PatternIndexer {
31
+ config;
32
+ rootDir;
33
+ constructor(rootDir, config = {}) {
34
+ this.rootDir = rootDir;
35
+ this.config = { ...DEFAULT_CONFIG, ...config };
36
+ }
37
+ async buildIndex() {
38
+ const startTime = Date.now();
39
+ // Find all files to index
40
+ const files = await this.findFiles();
41
+ // Process files in parallel batches (concurrency: 10)
42
+ const BATCH_SIZE = 10;
43
+ const patterns = [];
44
+ const indexedFiles = [];
45
+ for (let i = 0; i < files.length; i += BATCH_SIZE) {
46
+ const batch = files.slice(i, i + BATCH_SIZE);
47
+ const results = await Promise.all(batch.map(async (file) => {
48
+ try {
49
+ const relativePath = path.relative(this.rootDir, file);
50
+ const content = await fs.readFile(file, 'utf-8');
51
+ const fileHash = this.hashContent(content);
52
+ const filePatterns = await this.extractPatterns(file, content);
53
+ return {
54
+ patterns: filePatterns,
55
+ fileInfo: {
56
+ path: relativePath,
57
+ hash: fileHash,
58
+ patternCount: filePatterns.length,
59
+ indexedAt: new Date().toISOString()
60
+ }
61
+ };
62
+ }
63
+ catch (error) {
64
+ console.error(`Error indexing ${file}:`, error);
65
+ return null;
66
+ }
67
+ }));
68
+ for (const result of results) {
69
+ if (result) {
70
+ patterns.push(...result.patterns);
71
+ indexedFiles.push(result.fileInfo);
72
+ }
73
+ }
74
+ }
75
+ // Generate embeddings in parallel batches if enabled
76
+ if (this.config.useEmbeddings && patterns.length > 0) {
77
+ console.log(`Generating embeddings for ${patterns.length} patterns...`);
78
+ for (let i = 0; i < patterns.length; i += BATCH_SIZE) {
79
+ const batch = patterns.slice(i, i + BATCH_SIZE);
80
+ await Promise.all(batch.map(async (pattern) => {
81
+ pattern.embedding = await generateEmbedding(`${pattern.name} ${pattern.type} ${pattern.description}`);
82
+ }));
83
+ }
84
+ }
85
+ const endTime = Date.now();
86
+ const stats = this.calculateStats(patterns, indexedFiles, endTime - startTime);
87
+ const index = {
88
+ version: INDEX_VERSION,
89
+ lastUpdated: new Date().toISOString(),
90
+ rootDir: this.rootDir,
91
+ patterns,
92
+ stats,
93
+ files: indexedFiles
94
+ };
95
+ return index;
96
+ }
97
+ /**
98
+ * Incremental index update - only reindex changed files.
99
+ */
100
+ async updateIndex(existingIndex) {
101
+ const startTime = Date.now();
102
+ const files = await this.findFiles();
103
+ const updatedPatterns = [];
104
+ const updatedFiles = [];
105
+ // Create a map of existing file hashes
106
+ const existingFileMap = new Map(existingIndex.files.map(f => [f.path, f]));
107
+ // Process files in parallel batches (concurrency: 10)
108
+ const BATCH_SIZE = 10;
109
+ for (let i = 0; i < files.length; i += BATCH_SIZE) {
110
+ const batch = files.slice(i, i + BATCH_SIZE);
111
+ const results = await Promise.all(batch.map(async (file) => {
112
+ const relativePath = path.relative(this.rootDir, file);
113
+ const content = await fs.readFile(file, 'utf-8');
114
+ const fileHash = this.hashContent(content);
115
+ const existingFile = existingFileMap.get(relativePath);
116
+ if (existingFile && existingFile.hash === fileHash) {
117
+ // File unchanged, keep existing patterns
118
+ const existingPatterns = existingIndex.patterns.filter(p => p.file === relativePath);
119
+ return { patterns: existingPatterns, fileInfo: existingFile };
120
+ }
121
+ else {
122
+ // File changed or new, reindex
123
+ const filePatterns = await this.extractPatterns(file, content);
124
+ return {
125
+ patterns: filePatterns,
126
+ fileInfo: {
127
+ path: relativePath,
128
+ hash: fileHash,
129
+ patternCount: filePatterns.length,
130
+ indexedAt: new Date().toISOString()
131
+ }
132
+ };
133
+ }
134
+ }));
135
+ for (const result of results) {
136
+ updatedPatterns.push(...result.patterns);
137
+ updatedFiles.push(result.fileInfo);
138
+ }
139
+ }
140
+ // Update embeddings for new/changed patterns if enabled
141
+ if (this.config.useEmbeddings && updatedPatterns.length > 0) {
142
+ for (let i = 0; i < updatedPatterns.length; i += BATCH_SIZE) {
143
+ const batch = updatedPatterns.slice(i, i + BATCH_SIZE);
144
+ await Promise.all(batch.map(async (pattern) => {
145
+ if (!pattern.embedding) {
146
+ pattern.embedding = await generateEmbedding(`${pattern.name} ${pattern.type} ${pattern.description}`);
147
+ }
148
+ }));
149
+ }
150
+ }
151
+ const endTime = Date.now();
152
+ const stats = this.calculateStats(updatedPatterns, updatedFiles, endTime - startTime);
153
+ return {
154
+ version: INDEX_VERSION,
155
+ lastUpdated: new Date().toISOString(),
156
+ rootDir: this.rootDir,
157
+ patterns: updatedPatterns,
158
+ stats,
159
+ files: updatedFiles
160
+ };
161
+ }
162
+ /**
163
+ * Find all files to index based on configuration.
164
+ */
165
+ async findFiles() {
166
+ const patterns = this.config.include.map(p => this.config.extensions.map(ext => p.endsWith('*') ? `${p}${ext}` : p)).flat();
167
+ let exclude = [...this.config.exclude];
168
+ if (!this.config.indexTests) {
169
+ exclude.push('**/*.test.*', '**/*.spec.*', '**/__tests__/**');
170
+ }
171
+ const files = await globby(patterns, {
172
+ cwd: this.rootDir,
173
+ absolute: true,
174
+ ignore: exclude,
175
+ gitignore: true
176
+ });
177
+ return files;
178
+ }
179
+ /**
180
+ * Extract patterns from a single file using TypeScript AST.
181
+ */
182
+ async extractPatterns(filePath, content) {
183
+ const patterns = [];
184
+ const relativePath = path.relative(this.rootDir, filePath);
185
+ // Parse with TypeScript
186
+ const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true, this.getScriptKind(filePath));
187
+ // Walk the AST
188
+ const visit = (node) => {
189
+ const pattern = this.nodeToPattern(node, sourceFile, relativePath, content);
190
+ if (pattern) {
191
+ patterns.push(pattern);
192
+ }
193
+ ts.forEachChild(node, visit);
194
+ };
195
+ visit(sourceFile);
196
+ return patterns;
197
+ }
198
+ /**
199
+ * Convert an AST node to a PatternEntry if applicable.
200
+ */
201
+ nodeToPattern(node, sourceFile, filePath, content) {
202
+ const startPos = sourceFile.getLineAndCharacterOfPosition(node.getStart());
203
+ const endPos = sourceFile.getLineAndCharacterOfPosition(node.getEnd());
204
+ const line = startPos.line + 1;
205
+ const endLine = endPos.line + 1;
206
+ // Function declarations
207
+ if (ts.isFunctionDeclaration(node) && node.name) {
208
+ const name = node.name.text;
209
+ if (name.length < this.config.minNameLength)
210
+ return null;
211
+ return this.createPatternEntry({
212
+ type: this.detectFunctionType(name, node),
213
+ name,
214
+ file: filePath,
215
+ line,
216
+ endLine,
217
+ signature: this.getFunctionSignature(node, sourceFile),
218
+ description: this.getJSDocDescription(node, sourceFile),
219
+ keywords: this.extractKeywords(name),
220
+ content: node.getText(sourceFile),
221
+ exported: this.isExported(node)
222
+ });
223
+ }
224
+ // Variable declarations with arrow functions
225
+ if (ts.isVariableStatement(node)) {
226
+ const patterns = [];
227
+ for (const decl of node.declarationList.declarations) {
228
+ if (ts.isIdentifier(decl.name) && decl.initializer) {
229
+ const name = decl.name.text;
230
+ if (name.length < this.config.minNameLength)
231
+ continue;
232
+ if (ts.isArrowFunction(decl.initializer) || ts.isFunctionExpression(decl.initializer)) {
233
+ return this.createPatternEntry({
234
+ type: this.detectFunctionType(name, decl.initializer),
235
+ name,
236
+ file: filePath,
237
+ line,
238
+ endLine,
239
+ signature: this.getArrowFunctionSignature(decl.initializer, sourceFile),
240
+ description: this.getJSDocDescription(node, sourceFile),
241
+ keywords: this.extractKeywords(name),
242
+ content: node.getText(sourceFile),
243
+ exported: this.isExported(node)
244
+ });
245
+ }
246
+ // Constants
247
+ if (ts.isStringLiteral(decl.initializer) ||
248
+ ts.isNumericLiteral(decl.initializer) ||
249
+ ts.isObjectLiteralExpression(decl.initializer)) {
250
+ const isConstant = node.declarationList.flags & ts.NodeFlags.Const;
251
+ if (isConstant && name === name.toUpperCase()) {
252
+ return this.createPatternEntry({
253
+ type: 'constant',
254
+ name,
255
+ file: filePath,
256
+ line,
257
+ endLine,
258
+ signature: '',
259
+ description: this.getJSDocDescription(node, sourceFile),
260
+ keywords: this.extractKeywords(name),
261
+ content: node.getText(sourceFile),
262
+ exported: this.isExported(node)
263
+ });
264
+ }
265
+ }
266
+ }
267
+ }
268
+ }
269
+ // Class declarations
270
+ if (ts.isClassDeclaration(node) && node.name) {
271
+ const name = node.name.text;
272
+ if (name.length < this.config.minNameLength)
273
+ return null;
274
+ return this.createPatternEntry({
275
+ type: this.detectClassType(name, node),
276
+ name,
277
+ file: filePath,
278
+ line,
279
+ endLine,
280
+ signature: this.getClassSignature(node, sourceFile),
281
+ description: this.getJSDocDescription(node, sourceFile),
282
+ keywords: this.extractKeywords(name),
283
+ content: node.getText(sourceFile),
284
+ exported: this.isExported(node)
285
+ });
286
+ }
287
+ // Interface declarations
288
+ if (ts.isInterfaceDeclaration(node)) {
289
+ const name = node.name.text;
290
+ if (name.length < this.config.minNameLength)
291
+ return null;
292
+ return this.createPatternEntry({
293
+ type: 'interface',
294
+ name,
295
+ file: filePath,
296
+ line,
297
+ endLine,
298
+ signature: this.getInterfaceSignature(node, sourceFile),
299
+ description: this.getJSDocDescription(node, sourceFile),
300
+ keywords: this.extractKeywords(name),
301
+ content: node.getText(sourceFile),
302
+ exported: this.isExported(node)
303
+ });
304
+ }
305
+ // Type alias declarations
306
+ if (ts.isTypeAliasDeclaration(node)) {
307
+ const name = node.name.text;
308
+ if (name.length < this.config.minNameLength)
309
+ return null;
310
+ return this.createPatternEntry({
311
+ type: 'type',
312
+ name,
313
+ file: filePath,
314
+ line,
315
+ endLine,
316
+ signature: node.getText(sourceFile).split('=')[0].trim(),
317
+ description: this.getJSDocDescription(node, sourceFile),
318
+ keywords: this.extractKeywords(name),
319
+ content: node.getText(sourceFile),
320
+ exported: this.isExported(node)
321
+ });
322
+ }
323
+ // Enum declarations
324
+ if (ts.isEnumDeclaration(node)) {
325
+ const name = node.name.text;
326
+ if (name.length < this.config.minNameLength)
327
+ return null;
328
+ return this.createPatternEntry({
329
+ type: 'enum',
330
+ name,
331
+ file: filePath,
332
+ line,
333
+ endLine,
334
+ signature: `enum ${name}`,
335
+ description: this.getJSDocDescription(node, sourceFile),
336
+ keywords: this.extractKeywords(name),
337
+ content: node.getText(sourceFile),
338
+ exported: this.isExported(node)
339
+ });
340
+ }
341
+ return null;
342
+ }
343
+ /**
344
+ * Detect the specific type of a function based on naming conventions.
345
+ */
346
+ detectFunctionType(name, node) {
347
+ // React hooks
348
+ if (name.startsWith('use') && name.length > 3 && name[3] === name[3].toUpperCase()) {
349
+ return 'hook';
350
+ }
351
+ // React components (PascalCase and returns JSX)
352
+ if (name[0] === name[0].toUpperCase() && this.containsJSX(node)) {
353
+ return 'component';
354
+ }
355
+ // Middleware patterns
356
+ if (name.includes('Middleware') || name.includes('middleware')) {
357
+ return 'middleware';
358
+ }
359
+ // Handler patterns
360
+ if (name.includes('Handler') || name.includes('handler')) {
361
+ return 'handler';
362
+ }
363
+ // Factory patterns
364
+ if (name.startsWith('create') || name.startsWith('make') || name.includes('Factory')) {
365
+ return 'factory';
366
+ }
367
+ return 'function';
368
+ }
369
+ /**
370
+ * Detect the specific type of a class.
371
+ */
372
+ detectClassType(name, node) {
373
+ // Error classes
374
+ if (name.endsWith('Error') || name.endsWith('Exception')) {
375
+ return 'error';
376
+ }
377
+ // Check for React component (extends Component/PureComponent)
378
+ if (node.heritageClauses) {
379
+ for (const clause of node.heritageClauses) {
380
+ const text = clause.getText();
381
+ if (text.includes('Component') || text.includes('PureComponent')) {
382
+ return 'component';
383
+ }
384
+ }
385
+ }
386
+ // Store patterns
387
+ if (name.endsWith('Store') || name.endsWith('State')) {
388
+ return 'store';
389
+ }
390
+ // Model patterns
391
+ if (name.endsWith('Model') || name.endsWith('Entity')) {
392
+ return 'model';
393
+ }
394
+ return 'class';
395
+ }
396
+ /**
397
+ * Check if a node contains JSX.
398
+ */
399
+ containsJSX(node) {
400
+ let hasJSX = false;
401
+ const visit = (n) => {
402
+ if (ts.isJsxElement(n) || ts.isJsxSelfClosingElement(n) || ts.isJsxFragment(n)) {
403
+ hasJSX = true;
404
+ return;
405
+ }
406
+ ts.forEachChild(n, visit);
407
+ };
408
+ visit(node);
409
+ return hasJSX;
410
+ }
411
+ /**
412
+ * Get function signature.
413
+ */
414
+ getFunctionSignature(node, sourceFile) {
415
+ const params = node.parameters
416
+ .map(p => p.getText(sourceFile))
417
+ .join(', ');
418
+ const returnType = node.type ? `: ${node.type.getText(sourceFile)}` : '';
419
+ return `(${params})${returnType}`;
420
+ }
421
+ /**
422
+ * Get arrow function signature.
423
+ */
424
+ getArrowFunctionSignature(node, sourceFile) {
425
+ const params = node.parameters
426
+ .map(p => p.getText(sourceFile))
427
+ .join(', ');
428
+ const returnType = node.type ? `: ${node.type.getText(sourceFile)}` : '';
429
+ return `(${params})${returnType}`;
430
+ }
431
+ /**
432
+ * Get class signature.
433
+ */
434
+ getClassSignature(node, sourceFile) {
435
+ let sig = `class ${node.name?.text || 'Anonymous'}`;
436
+ if (node.heritageClauses) {
437
+ sig += ' ' + node.heritageClauses.map(c => c.getText(sourceFile)).join(' ');
438
+ }
439
+ return sig;
440
+ }
441
+ /**
442
+ * Get interface signature.
443
+ */
444
+ getInterfaceSignature(node, sourceFile) {
445
+ let sig = `interface ${node.name.text}`;
446
+ if (node.typeParameters) {
447
+ sig += `<${node.typeParameters.map(p => p.getText(sourceFile)).join(', ')}>`;
448
+ }
449
+ return sig;
450
+ }
451
+ /**
452
+ * Extract JSDoc description from a node.
453
+ */
454
+ getJSDocDescription(node, sourceFile) {
455
+ const jsDocTags = ts.getJSDocTags(node);
456
+ const jsDocComment = ts.getJSDocCommentsAndTags(node);
457
+ for (const tag of jsDocComment) {
458
+ if (ts.isJSDoc(tag) && tag.comment) {
459
+ if (typeof tag.comment === 'string') {
460
+ return tag.comment;
461
+ }
462
+ return tag.comment.map(c => c.getText(sourceFile)).join(' ');
463
+ }
464
+ }
465
+ return '';
466
+ }
467
+ /**
468
+ * Check if a node is exported.
469
+ */
470
+ isExported(node) {
471
+ if (ts.canHaveModifiers(node)) {
472
+ const modifiers = ts.getModifiers(node);
473
+ if (modifiers) {
474
+ return modifiers.some(m => m.kind === ts.SyntaxKind.ExportKeyword);
475
+ }
476
+ }
477
+ return false;
478
+ }
479
+ /**
480
+ * Extract keywords from a name for semantic matching.
481
+ */
482
+ extractKeywords(name) {
483
+ // Split camelCase and PascalCase
484
+ const words = name
485
+ .replace(/([a-z])([A-Z])/g, '$1 $2')
486
+ .replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2')
487
+ .toLowerCase()
488
+ .split(/[\s_-]+/)
489
+ .filter(w => w.length > 1);
490
+ return [...new Set(words)];
491
+ }
492
+ /**
493
+ * Create a PatternEntry with computed fields.
494
+ */
495
+ createPatternEntry(params) {
496
+ const id = this.hashContent(`${params.file}:${params.name}:${params.line}`);
497
+ const hash = this.hashContent(params.content);
498
+ return {
499
+ id,
500
+ type: params.type,
501
+ name: params.name,
502
+ file: params.file,
503
+ line: params.line,
504
+ endLine: params.endLine,
505
+ signature: params.signature,
506
+ description: params.description,
507
+ keywords: params.keywords,
508
+ hash,
509
+ exported: params.exported,
510
+ usageCount: 0, // Will be calculated in a separate pass
511
+ indexedAt: new Date().toISOString()
512
+ };
513
+ }
514
+ /**
515
+ * Get the TypeScript ScriptKind for a file.
516
+ */
517
+ getScriptKind(filePath) {
518
+ const ext = path.extname(filePath).toLowerCase();
519
+ switch (ext) {
520
+ case '.ts': return ts.ScriptKind.TS;
521
+ case '.tsx': return ts.ScriptKind.TSX;
522
+ case '.js': return ts.ScriptKind.JS;
523
+ case '.jsx': return ts.ScriptKind.JSX;
524
+ default: return ts.ScriptKind.TS;
525
+ }
526
+ }
527
+ /**
528
+ * Calculate index statistics.
529
+ */
530
+ calculateStats(patterns, files, durationMs) {
531
+ const byType = {};
532
+ for (const pattern of patterns) {
533
+ byType[pattern.type] = (byType[pattern.type] || 0) + 1;
534
+ }
535
+ return {
536
+ totalPatterns: patterns.length,
537
+ totalFiles: files.length,
538
+ byType: byType,
539
+ indexDurationMs: durationMs
540
+ };
541
+ }
542
+ /**
543
+ * Hash content using SHA-256.
544
+ */
545
+ hashContent(content) {
546
+ return createHash('sha256').update(content).digest('hex').slice(0, 16);
547
+ }
548
+ }
549
+ /**
550
+ * Save a pattern index to disk.
551
+ */
552
+ export async function savePatternIndex(index, outputPath) {
553
+ await fs.mkdir(path.dirname(outputPath), { recursive: true });
554
+ await fs.writeFile(outputPath, JSON.stringify(index, null, 2), 'utf-8');
555
+ }
556
+ /**
557
+ * Load a pattern index from disk.
558
+ */
559
+ export async function loadPatternIndex(indexPath) {
560
+ try {
561
+ const content = await fs.readFile(indexPath, 'utf-8');
562
+ return JSON.parse(content);
563
+ }
564
+ catch {
565
+ return null;
566
+ }
567
+ }
568
+ /**
569
+ * Get the default index path for a project.
570
+ */
571
+ export function getDefaultIndexPath(rootDir) {
572
+ return path.join(rootDir, '.rigour', 'patterns.json');
573
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Pattern Indexer Tests
3
+ *
4
+ * Comprehensive tests for the pattern indexer.
5
+ */
6
+ export {};