@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.
- package/dist/index.js +4 -0
- package/dist/pattern-index/embeddings.d.ts +19 -0
- package/dist/pattern-index/embeddings.js +78 -0
- package/dist/pattern-index/index.d.ts +11 -0
- package/dist/pattern-index/index.js +15 -0
- package/dist/pattern-index/indexer.d.ts +101 -0
- package/dist/pattern-index/indexer.js +573 -0
- package/dist/pattern-index/indexer.test.d.ts +6 -0
- package/dist/pattern-index/indexer.test.js +188 -0
- package/dist/pattern-index/matcher.d.ts +106 -0
- package/dist/pattern-index/matcher.js +376 -0
- package/dist/pattern-index/matcher.test.d.ts +6 -0
- package/dist/pattern-index/matcher.test.js +238 -0
- package/dist/pattern-index/overrides.d.ts +64 -0
- package/dist/pattern-index/overrides.js +196 -0
- package/dist/pattern-index/security.d.ts +25 -0
- package/dist/pattern-index/security.js +127 -0
- package/dist/pattern-index/staleness.d.ts +71 -0
- package/dist/pattern-index/staleness.js +381 -0
- package/dist/pattern-index/staleness.test.d.ts +6 -0
- package/dist/pattern-index/staleness.test.js +211 -0
- package/dist/pattern-index/types.d.ts +221 -0
- package/dist/pattern-index/types.js +7 -0
- package/package.json +14 -1
- package/src/index.ts +4 -0
- package/src/pattern-index/embeddings.ts +84 -0
- package/src/pattern-index/index.ts +53 -0
- package/src/pattern-index/indexer.test.ts +276 -0
- package/src/pattern-index/indexer.ts +693 -0
- package/src/pattern-index/matcher.test.ts +293 -0
- package/src/pattern-index/matcher.ts +493 -0
- package/src/pattern-index/overrides.ts +235 -0
- package/src/pattern-index/security.ts +151 -0
- package/src/pattern-index/staleness.test.ts +313 -0
- package/src/pattern-index/staleness.ts +438 -0
- package/src/pattern-index/types.ts +339 -0
- package/vitest.config.ts +7 -0
- 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
|
+
}
|