@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.
- package/README.md +58 -0
- package/dist/context.test.js +2 -3
- package/dist/environment.test.js +2 -1
- package/dist/gates/agent-team.d.ts +2 -1
- package/dist/gates/agent-team.js +1 -0
- package/dist/gates/base.d.ts +3 -1
- package/dist/gates/base.js +3 -0
- package/dist/gates/checkpoint.d.ts +2 -1
- package/dist/gates/checkpoint.js +3 -2
- package/dist/gates/context-window-artifacts.d.ts +2 -1
- package/dist/gates/context-window-artifacts.js +6 -3
- package/dist/gates/context.d.ts +2 -1
- package/dist/gates/context.js +1 -0
- package/dist/gates/coverage.js +3 -1
- package/dist/gates/dependency.js +5 -5
- package/dist/gates/duplication-drift.d.ts +2 -1
- package/dist/gates/duplication-drift.js +4 -1
- package/dist/gates/environment.js +4 -4
- package/dist/gates/hallucinated-imports.d.ts +21 -2
- package/dist/gates/hallucinated-imports.js +116 -2
- package/dist/gates/inconsistent-error-handling.d.ts +2 -1
- package/dist/gates/inconsistent-error-handling.js +21 -7
- package/dist/gates/promise-safety.d.ts +68 -0
- package/dist/gates/promise-safety.js +509 -0
- package/dist/gates/retry-loop-breaker.d.ts +2 -1
- package/dist/gates/retry-loop-breaker.js +2 -1
- package/dist/gates/runner.js +34 -1
- package/dist/gates/safety.d.ts +2 -1
- package/dist/gates/safety.js +2 -1
- package/dist/gates/security-patterns.d.ts +2 -1
- package/dist/gates/security-patterns.js +1 -0
- package/dist/gates/structure.js +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/services/fix-packet-service.d.ts +0 -1
- package/dist/services/fix-packet-service.js +9 -14
- package/dist/services/score-history.d.ts +54 -0
- package/dist/services/score-history.js +122 -0
- package/dist/templates/index.js +169 -0
- package/dist/types/fix-packet.d.ts +5 -5
- package/dist/types/fix-packet.js +1 -1
- package/dist/types/index.d.ts +153 -0
- package/dist/types/index.js +19 -0
- package/package.json +21 -1
- package/src/context.test.ts +0 -256
- package/src/discovery.test.ts +0 -88
- package/src/discovery.ts +0 -112
- package/src/environment.test.ts +0 -115
- package/src/gates/agent-team.test.ts +0 -134
- package/src/gates/agent-team.ts +0 -210
- package/src/gates/ast-handlers/base.ts +0 -13
- package/src/gates/ast-handlers/python.ts +0 -145
- package/src/gates/ast-handlers/python_parser.py +0 -181
- package/src/gates/ast-handlers/typescript.ts +0 -264
- package/src/gates/ast-handlers/universal.ts +0 -184
- package/src/gates/ast.ts +0 -54
- package/src/gates/base.ts +0 -28
- package/src/gates/checkpoint.test.ts +0 -135
- package/src/gates/checkpoint.ts +0 -311
- package/src/gates/content.ts +0 -51
- package/src/gates/context-window-artifacts.ts +0 -277
- package/src/gates/context.ts +0 -270
- package/src/gates/coverage.ts +0 -74
- package/src/gates/dependency.ts +0 -108
- package/src/gates/duplication-drift.ts +0 -231
- package/src/gates/environment.ts +0 -94
- package/src/gates/file.ts +0 -46
- package/src/gates/hallucinated-imports.ts +0 -361
- package/src/gates/inconsistent-error-handling.ts +0 -254
- package/src/gates/retry-loop-breaker.ts +0 -151
- package/src/gates/runner.ts +0 -188
- package/src/gates/safety.ts +0 -56
- package/src/gates/security-patterns.test.ts +0 -162
- package/src/gates/security-patterns.ts +0 -306
- package/src/gates/structure.ts +0 -36
- package/src/index.ts +0 -13
- package/src/pattern-index/embeddings.ts +0 -84
- package/src/pattern-index/index.ts +0 -59
- package/src/pattern-index/indexer.test.ts +0 -276
- package/src/pattern-index/indexer.ts +0 -1023
- package/src/pattern-index/matcher.test.ts +0 -293
- package/src/pattern-index/matcher.ts +0 -493
- package/src/pattern-index/overrides.ts +0 -235
- package/src/pattern-index/security.ts +0 -151
- package/src/pattern-index/staleness.test.ts +0 -313
- package/src/pattern-index/staleness.ts +0 -568
- package/src/pattern-index/types.ts +0 -339
- package/src/safety.test.ts +0 -53
- package/src/services/adaptive-thresholds.test.ts +0 -189
- package/src/services/adaptive-thresholds.ts +0 -275
- package/src/services/context-engine.ts +0 -104
- package/src/services/fix-packet-service.ts +0 -42
- package/src/services/state-service.ts +0 -138
- package/src/smoke.test.ts +0 -18
- package/src/templates/index.ts +0 -338
- package/src/types/fix-packet.ts +0 -32
- package/src/types/index.ts +0 -200
- package/src/utils/logger.ts +0 -43
- package/src/utils/scanner.test.ts +0 -37
- package/src/utils/scanner.ts +0 -43
- package/tsconfig.json +0 -10
- package/vitest.config.ts +0 -7
- 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
|
-
}
|