rlm-analyzer 1.5.1 → 1.7.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/CHANGELOG.md +55 -0
- package/README.md +35 -0
- package/dist/analyzer.d.ts +32 -2
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +132 -7
- package/dist/analyzer.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +57 -4
- package/dist/cli.js.map +1 -1
- package/dist/file-chunker.d.ts +61 -0
- package/dist/file-chunker.d.ts.map +1 -0
- package/dist/file-chunker.js +256 -0
- package/dist/file-chunker.js.map +1 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/mcp-server.js +1 -1
- package/dist/orchestrator.d.ts +6 -0
- package/dist/orchestrator.d.ts.map +1 -1
- package/dist/orchestrator.js +79 -6
- package/dist/orchestrator.js.map +1 -1
- package/dist/prompts.d.ts +5 -0
- package/dist/prompts.d.ts.map +1 -1
- package/dist/prompts.js +12 -0
- package/dist/prompts.js.map +1 -1
- package/dist/structural-index.d.ts +89 -0
- package/dist/structural-index.d.ts.map +1 -0
- package/dist/structural-index.js +721 -0
- package/dist/structural-index.js.map +1 -0
- package/dist/supply-chain.d.ts +11 -0
- package/dist/supply-chain.d.ts.map +1 -0
- package/dist/supply-chain.js +359 -0
- package/dist/supply-chain.js.map +1 -0
- package/dist/types.d.ts +146 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +51 -14
- package/dist/types.js.map +1 -1
- package/package.json +9 -3
|
@@ -0,0 +1,721 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structural Index Module
|
|
3
|
+
* Caches file metadata, builds dependency graphs, and clusters related files
|
|
4
|
+
* for more efficient and accurate code analysis.
|
|
5
|
+
*
|
|
6
|
+
* Key features:
|
|
7
|
+
* - File hashing for change detection
|
|
8
|
+
* - Import/export extraction for dependency tracking
|
|
9
|
+
* - Dependency graph construction
|
|
10
|
+
* - File clustering for grouped analysis
|
|
11
|
+
* - Persistent caching to ~/.rlm-analyzer/cache/
|
|
12
|
+
*/
|
|
13
|
+
import * as fs from 'fs';
|
|
14
|
+
import * as path from 'path';
|
|
15
|
+
import * as crypto from 'crypto';
|
|
16
|
+
import * as os from 'os';
|
|
17
|
+
import { CODE_EXTENSIONS, INCLUDE_FILENAMES, IGNORE_DIRS } from './types.js';
|
|
18
|
+
/** Current index format version */
|
|
19
|
+
const INDEX_VERSION = '1.0.0';
|
|
20
|
+
/** Default max file size before chunking (100KB) */
|
|
21
|
+
const DEFAULT_MAX_FILE_SIZE = 100_000;
|
|
22
|
+
/** Default chunk size (50KB) */
|
|
23
|
+
const DEFAULT_CHUNK_SIZE = 50_000;
|
|
24
|
+
/** Cache directory */
|
|
25
|
+
const CACHE_DIR = path.join(os.homedir(), '.rlm-analyzer', 'cache');
|
|
26
|
+
// ============================================================================
|
|
27
|
+
// File Hashing
|
|
28
|
+
// ============================================================================
|
|
29
|
+
/**
|
|
30
|
+
* Calculate SHA-256 hash of file content
|
|
31
|
+
*/
|
|
32
|
+
export function hashContent(content) {
|
|
33
|
+
return crypto.createHash('sha256').update(content).digest('hex').slice(0, 16);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Calculate hash for a project directory (based on file paths and mtimes)
|
|
37
|
+
*/
|
|
38
|
+
export function hashProject(directory) {
|
|
39
|
+
const absPath = path.resolve(directory);
|
|
40
|
+
return crypto.createHash('sha256').update(absPath).digest('hex').slice(0, 12);
|
|
41
|
+
}
|
|
42
|
+
// ============================================================================
|
|
43
|
+
// Import/Export Extraction
|
|
44
|
+
// ============================================================================
|
|
45
|
+
/** Import patterns by language */
|
|
46
|
+
const IMPORT_PATTERNS = {
|
|
47
|
+
typescript: [
|
|
48
|
+
/import\s+.*?\s+from\s+['"]([^'"]+)['"]/g,
|
|
49
|
+
/import\s+['"]([^'"]+)['"]/g,
|
|
50
|
+
/require\s*\(\s*['"]([^'"]+)['"]\s*\)/g,
|
|
51
|
+
/import\s*\(\s*['"]([^'"]+)['"]\s*\)/g,
|
|
52
|
+
],
|
|
53
|
+
javascript: [
|
|
54
|
+
/import\s+.*?\s+from\s+['"]([^'"]+)['"]/g,
|
|
55
|
+
/import\s+['"]([^'"]+)['"]/g,
|
|
56
|
+
/require\s*\(\s*['"]([^'"]+)['"]\s*\)/g,
|
|
57
|
+
],
|
|
58
|
+
python: [
|
|
59
|
+
/^import\s+(\S+)/gm,
|
|
60
|
+
/^from\s+(\S+)\s+import/gm,
|
|
61
|
+
],
|
|
62
|
+
go: [
|
|
63
|
+
/import\s+["']([^"']+)["']/g,
|
|
64
|
+
/import\s+\(\s*\n([^)]+)\)/gs,
|
|
65
|
+
],
|
|
66
|
+
rust: [
|
|
67
|
+
/use\s+([a-zA-Z_][a-zA-Z0-9_:]*)/g,
|
|
68
|
+
/mod\s+([a-zA-Z_][a-zA-Z0-9_]*)/g,
|
|
69
|
+
],
|
|
70
|
+
java: [
|
|
71
|
+
/import\s+([a-zA-Z_][a-zA-Z0-9_.]*)/g,
|
|
72
|
+
],
|
|
73
|
+
csharp: [
|
|
74
|
+
/using\s+([a-zA-Z_][a-zA-Z0-9_.]*)/g,
|
|
75
|
+
],
|
|
76
|
+
ruby: [
|
|
77
|
+
/require\s+['"]([^'"]+)['"]/g,
|
|
78
|
+
/require_relative\s+['"]([^'"]+)['"]/g,
|
|
79
|
+
],
|
|
80
|
+
php: [
|
|
81
|
+
/use\s+([a-zA-Z_\\][a-zA-Z0-9_\\]*)/g,
|
|
82
|
+
/require(?:_once)?\s+['"]([^'"]+)['"]/g,
|
|
83
|
+
/include(?:_once)?\s+['"]([^'"]+)['"]/g,
|
|
84
|
+
],
|
|
85
|
+
};
|
|
86
|
+
/** Export patterns by language */
|
|
87
|
+
const EXPORT_PATTERNS = {
|
|
88
|
+
typescript: [
|
|
89
|
+
/export\s+(?:default\s+)?(?:class|function|const|let|var|interface|type|enum)\s+([a-zA-Z_][a-zA-Z0-9_]*)/g,
|
|
90
|
+
/export\s+\{\s*([^}]+)\s*\}/g,
|
|
91
|
+
],
|
|
92
|
+
javascript: [
|
|
93
|
+
/export\s+(?:default\s+)?(?:class|function|const|let|var)\s+([a-zA-Z_][a-zA-Z0-9_]*)/g,
|
|
94
|
+
/module\.exports\s*=\s*\{([^}]+)\}/g,
|
|
95
|
+
/exports\.([a-zA-Z_][a-zA-Z0-9_]*)/g,
|
|
96
|
+
],
|
|
97
|
+
python: [
|
|
98
|
+
/^(?:class|def)\s+([a-zA-Z_][a-zA-Z0-9_]*)/gm,
|
|
99
|
+
/__all__\s*=\s*\[([^\]]+)\]/g,
|
|
100
|
+
],
|
|
101
|
+
go: [
|
|
102
|
+
/^func\s+([A-Z][a-zA-Z0-9_]*)/gm,
|
|
103
|
+
/^type\s+([A-Z][a-zA-Z0-9_]*)/gm,
|
|
104
|
+
],
|
|
105
|
+
rust: [
|
|
106
|
+
/pub\s+(?:fn|struct|enum|trait|type|const|static)\s+([a-zA-Z_][a-zA-Z0-9_]*)/g,
|
|
107
|
+
],
|
|
108
|
+
java: [
|
|
109
|
+
/public\s+(?:class|interface|enum)\s+([a-zA-Z_][a-zA-Z0-9_]*)/g,
|
|
110
|
+
],
|
|
111
|
+
};
|
|
112
|
+
/**
|
|
113
|
+
* Get language from file extension
|
|
114
|
+
*/
|
|
115
|
+
function getLanguage(ext) {
|
|
116
|
+
const langMap = {
|
|
117
|
+
'.ts': 'typescript', '.tsx': 'typescript', '.mts': 'typescript',
|
|
118
|
+
'.js': 'javascript', '.jsx': 'javascript', '.mjs': 'javascript', '.cjs': 'javascript',
|
|
119
|
+
'.py': 'python', '.pyw': 'python',
|
|
120
|
+
'.go': 'go',
|
|
121
|
+
'.rs': 'rust',
|
|
122
|
+
'.java': 'java', '.kt': 'java', '.scala': 'java',
|
|
123
|
+
'.cs': 'csharp', '.fs': 'csharp',
|
|
124
|
+
'.rb': 'ruby',
|
|
125
|
+
'.php': 'php',
|
|
126
|
+
};
|
|
127
|
+
return langMap[ext] || 'unknown';
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Extract imports from file content
|
|
131
|
+
*/
|
|
132
|
+
export function extractImports(content, ext) {
|
|
133
|
+
const lang = getLanguage(ext);
|
|
134
|
+
const patterns = IMPORT_PATTERNS[lang] || IMPORT_PATTERNS.typescript;
|
|
135
|
+
const imports = new Set();
|
|
136
|
+
for (const pattern of patterns) {
|
|
137
|
+
// Reset regex state
|
|
138
|
+
pattern.lastIndex = 0;
|
|
139
|
+
let match;
|
|
140
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
141
|
+
const imported = match[1]?.trim();
|
|
142
|
+
if (imported) {
|
|
143
|
+
// Handle multiple imports in one statement
|
|
144
|
+
if (imported.includes(',')) {
|
|
145
|
+
imported.split(',').forEach(i => imports.add(i.trim()));
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
imports.add(imported);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return Array.from(imports);
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Extract exports from file content
|
|
157
|
+
*/
|
|
158
|
+
export function extractExports(content, ext) {
|
|
159
|
+
const lang = getLanguage(ext);
|
|
160
|
+
const patterns = EXPORT_PATTERNS[lang] || EXPORT_PATTERNS.typescript;
|
|
161
|
+
const exports = new Set();
|
|
162
|
+
for (const pattern of patterns) {
|
|
163
|
+
pattern.lastIndex = 0;
|
|
164
|
+
let match;
|
|
165
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
166
|
+
const exported = match[1]?.trim();
|
|
167
|
+
if (exported) {
|
|
168
|
+
if (exported.includes(',')) {
|
|
169
|
+
exported.split(',').forEach(e => {
|
|
170
|
+
const name = e.trim().split(/\s+as\s+/)[0].trim();
|
|
171
|
+
if (name)
|
|
172
|
+
exports.add(name);
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
exports.add(exported);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return Array.from(exports);
|
|
182
|
+
}
|
|
183
|
+
// ============================================================================
|
|
184
|
+
// Large File Chunking
|
|
185
|
+
// ============================================================================
|
|
186
|
+
/**
|
|
187
|
+
* Check if a file needs chunking
|
|
188
|
+
*/
|
|
189
|
+
export function needsChunking(size, maxSize = DEFAULT_MAX_FILE_SIZE) {
|
|
190
|
+
return size > maxSize;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Chunk a large file by lines
|
|
194
|
+
*/
|
|
195
|
+
export function chunkFile(content, chunkSize = DEFAULT_CHUNK_SIZE) {
|
|
196
|
+
const lines = content.split('\n');
|
|
197
|
+
const chunks = [];
|
|
198
|
+
let currentChunk = [];
|
|
199
|
+
let currentSize = 0;
|
|
200
|
+
let startLine = 0;
|
|
201
|
+
for (let i = 0; i < lines.length; i++) {
|
|
202
|
+
const line = lines[i];
|
|
203
|
+
const lineSize = line.length + 1; // +1 for newline
|
|
204
|
+
if (currentSize + lineSize > chunkSize && currentChunk.length > 0) {
|
|
205
|
+
// Save current chunk
|
|
206
|
+
chunks.push({
|
|
207
|
+
index: chunks.length,
|
|
208
|
+
total: 0, // Will be updated
|
|
209
|
+
startLine,
|
|
210
|
+
endLine: i - 1,
|
|
211
|
+
content: currentChunk.join('\n'),
|
|
212
|
+
});
|
|
213
|
+
currentChunk = [];
|
|
214
|
+
currentSize = 0;
|
|
215
|
+
startLine = i;
|
|
216
|
+
}
|
|
217
|
+
currentChunk.push(line);
|
|
218
|
+
currentSize += lineSize;
|
|
219
|
+
}
|
|
220
|
+
// Don't forget the last chunk
|
|
221
|
+
if (currentChunk.length > 0) {
|
|
222
|
+
chunks.push({
|
|
223
|
+
index: chunks.length,
|
|
224
|
+
total: 0,
|
|
225
|
+
startLine,
|
|
226
|
+
endLine: lines.length - 1,
|
|
227
|
+
content: currentChunk.join('\n'),
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
// Update total count
|
|
231
|
+
const total = chunks.length;
|
|
232
|
+
chunks.forEach(chunk => { chunk.total = total; });
|
|
233
|
+
return chunks;
|
|
234
|
+
}
|
|
235
|
+
// ============================================================================
|
|
236
|
+
// Dependency Graph
|
|
237
|
+
// ============================================================================
|
|
238
|
+
/**
|
|
239
|
+
* Resolve an import path to a file path
|
|
240
|
+
*/
|
|
241
|
+
function resolveImportPath(importPath, sourceFile, fileIndex) {
|
|
242
|
+
// Skip external packages
|
|
243
|
+
if (!importPath.startsWith('.') && !importPath.startsWith('/')) {
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
const sourceDir = path.dirname(sourceFile);
|
|
247
|
+
let resolved = path.join(sourceDir, importPath);
|
|
248
|
+
// Try common extensions
|
|
249
|
+
const extensions = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '', '/index.ts', '/index.js'];
|
|
250
|
+
for (const ext of extensions) {
|
|
251
|
+
const candidate = resolved + ext;
|
|
252
|
+
if (fileIndex[candidate]) {
|
|
253
|
+
return candidate;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Build dependency graph from file index
|
|
260
|
+
*/
|
|
261
|
+
export function buildDependencyGraph(fileIndex) {
|
|
262
|
+
const edges = [];
|
|
263
|
+
for (const [filePath, entry] of Object.entries(fileIndex)) {
|
|
264
|
+
for (const imp of entry.imports) {
|
|
265
|
+
const resolved = resolveImportPath(imp, filePath, fileIndex);
|
|
266
|
+
if (resolved) {
|
|
267
|
+
edges.push({
|
|
268
|
+
from: filePath,
|
|
269
|
+
to: resolved,
|
|
270
|
+
type: 'import',
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return edges;
|
|
276
|
+
}
|
|
277
|
+
// ============================================================================
|
|
278
|
+
// File Clustering
|
|
279
|
+
// ============================================================================
|
|
280
|
+
/**
|
|
281
|
+
* Find connected components in the dependency graph
|
|
282
|
+
*/
|
|
283
|
+
function findConnectedComponents(files, edges) {
|
|
284
|
+
const adjacency = new Map();
|
|
285
|
+
// Build undirected adjacency list
|
|
286
|
+
for (const file of files) {
|
|
287
|
+
adjacency.set(file, new Set());
|
|
288
|
+
}
|
|
289
|
+
for (const edge of edges) {
|
|
290
|
+
adjacency.get(edge.from)?.add(edge.to);
|
|
291
|
+
adjacency.get(edge.to)?.add(edge.from);
|
|
292
|
+
}
|
|
293
|
+
const visited = new Set();
|
|
294
|
+
const components = [];
|
|
295
|
+
function dfs(node, component) {
|
|
296
|
+
if (visited.has(node))
|
|
297
|
+
return;
|
|
298
|
+
visited.add(node);
|
|
299
|
+
component.push(node);
|
|
300
|
+
for (const neighbor of adjacency.get(node) || []) {
|
|
301
|
+
dfs(neighbor, component);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
for (const file of files) {
|
|
305
|
+
if (!visited.has(file)) {
|
|
306
|
+
const component = [];
|
|
307
|
+
dfs(file, component);
|
|
308
|
+
if (component.length > 0) {
|
|
309
|
+
components.push(component);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return components;
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Detect cluster type based on file paths and content
|
|
317
|
+
*/
|
|
318
|
+
function detectClusterType(files) {
|
|
319
|
+
const patterns = {
|
|
320
|
+
test: [/test/, /spec/, /__tests__/],
|
|
321
|
+
config: [/config/, /\.config\./, /settings/],
|
|
322
|
+
utility: [/util/, /helper/, /lib/, /common/],
|
|
323
|
+
};
|
|
324
|
+
for (const file of files) {
|
|
325
|
+
const lower = file.toLowerCase();
|
|
326
|
+
if (patterns.test.some(p => p.test(lower)))
|
|
327
|
+
return 'test';
|
|
328
|
+
if (patterns.config.some(p => p.test(lower)))
|
|
329
|
+
return 'config';
|
|
330
|
+
if (patterns.utility.some(p => p.test(lower)))
|
|
331
|
+
return 'utility';
|
|
332
|
+
}
|
|
333
|
+
return files.length > 5 ? 'feature' : 'module';
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Find entry points in a cluster (files that are imported but don't import much)
|
|
337
|
+
*/
|
|
338
|
+
function findEntryPoints(clusterFiles, edges) {
|
|
339
|
+
const clusterSet = new Set(clusterFiles);
|
|
340
|
+
const importedBy = new Map();
|
|
341
|
+
const imports = new Map();
|
|
342
|
+
for (const file of clusterFiles) {
|
|
343
|
+
importedBy.set(file, 0);
|
|
344
|
+
imports.set(file, 0);
|
|
345
|
+
}
|
|
346
|
+
for (const edge of edges) {
|
|
347
|
+
if (clusterSet.has(edge.from) && clusterSet.has(edge.to)) {
|
|
348
|
+
importedBy.set(edge.to, (importedBy.get(edge.to) || 0) + 1);
|
|
349
|
+
imports.set(edge.from, (imports.get(edge.from) || 0) + 1);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
// Entry points: high imports count, low imported-by count
|
|
353
|
+
return clusterFiles
|
|
354
|
+
.filter(f => (importedBy.get(f) || 0) > 0 || clusterFiles.length === 1)
|
|
355
|
+
.sort((a, b) => {
|
|
356
|
+
const scoreA = (importedBy.get(a) || 0) - (imports.get(a) || 0);
|
|
357
|
+
const scoreB = (importedBy.get(b) || 0) - (imports.get(b) || 0);
|
|
358
|
+
return scoreB - scoreA;
|
|
359
|
+
})
|
|
360
|
+
.slice(0, 3);
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Build file clusters from dependency graph
|
|
364
|
+
*/
|
|
365
|
+
export function buildClusters(fileIndex, edges) {
|
|
366
|
+
const files = Object.keys(fileIndex);
|
|
367
|
+
const components = findConnectedComponents(files, edges);
|
|
368
|
+
return components.map((component, i) => {
|
|
369
|
+
const totalSize = component.reduce((sum, f) => sum + (fileIndex[f]?.size || 0), 0);
|
|
370
|
+
return {
|
|
371
|
+
id: `cluster-${i}`,
|
|
372
|
+
files: component,
|
|
373
|
+
entryPoints: findEntryPoints(component, edges),
|
|
374
|
+
totalSize,
|
|
375
|
+
type: detectClusterType(component),
|
|
376
|
+
};
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
// ============================================================================
|
|
380
|
+
// Cache Management
|
|
381
|
+
// ============================================================================
|
|
382
|
+
/**
|
|
383
|
+
* Get cache path for a project
|
|
384
|
+
*/
|
|
385
|
+
export function getCachePath(directory) {
|
|
386
|
+
const projectHash = hashProject(directory);
|
|
387
|
+
return path.join(CACHE_DIR, projectHash, 'index.json');
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* Load cached index if valid
|
|
391
|
+
*/
|
|
392
|
+
export function loadCachedIndex(directory) {
|
|
393
|
+
const cachePath = getCachePath(directory);
|
|
394
|
+
if (!fs.existsSync(cachePath)) {
|
|
395
|
+
return null;
|
|
396
|
+
}
|
|
397
|
+
try {
|
|
398
|
+
const data = fs.readFileSync(cachePath, 'utf-8');
|
|
399
|
+
const index = JSON.parse(data);
|
|
400
|
+
// Validate version
|
|
401
|
+
if (index.version !== INDEX_VERSION) {
|
|
402
|
+
return null;
|
|
403
|
+
}
|
|
404
|
+
// Check if project root matches
|
|
405
|
+
if (path.resolve(index.projectRoot) !== path.resolve(directory)) {
|
|
406
|
+
return null;
|
|
407
|
+
}
|
|
408
|
+
return index;
|
|
409
|
+
}
|
|
410
|
+
catch {
|
|
411
|
+
return null;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
/**
|
|
415
|
+
* Save index to cache
|
|
416
|
+
*/
|
|
417
|
+
export function saveIndexToCache(index) {
|
|
418
|
+
const cachePath = getCachePath(index.projectRoot);
|
|
419
|
+
const cacheDir = path.dirname(cachePath);
|
|
420
|
+
// Ensure cache directory exists
|
|
421
|
+
fs.mkdirSync(cacheDir, { recursive: true });
|
|
422
|
+
fs.writeFileSync(cachePath, JSON.stringify(index, null, 2));
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* Clear cache for a project
|
|
426
|
+
*/
|
|
427
|
+
export function clearCache(directory) {
|
|
428
|
+
const cachePath = getCachePath(directory);
|
|
429
|
+
const cacheDir = path.dirname(cachePath);
|
|
430
|
+
if (fs.existsSync(cacheDir)) {
|
|
431
|
+
fs.rmSync(cacheDir, { recursive: true });
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
// ============================================================================
|
|
435
|
+
// Main Indexing Function
|
|
436
|
+
// ============================================================================
|
|
437
|
+
/**
|
|
438
|
+
* Check if a file should be included
|
|
439
|
+
*/
|
|
440
|
+
function shouldIncludeFile(filePath, fileName) {
|
|
441
|
+
// Check by filename first
|
|
442
|
+
if (INCLUDE_FILENAMES.includes(fileName)) {
|
|
443
|
+
return true;
|
|
444
|
+
}
|
|
445
|
+
// Check by extension
|
|
446
|
+
const ext = path.extname(filePath);
|
|
447
|
+
return CODE_EXTENSIONS.some(e => ext === e || filePath.endsWith(e));
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* Build or update structural index for a directory
|
|
451
|
+
*/
|
|
452
|
+
export async function buildStructuralIndex(directory, options = {}) {
|
|
453
|
+
const { force = false, maxFileSize = DEFAULT_MAX_FILE_SIZE, chunkSize = DEFAULT_CHUNK_SIZE, buildDependencyGraph: buildDeps = true, enableClustering = true, verbose = false, } = options;
|
|
454
|
+
const absDir = path.resolve(directory);
|
|
455
|
+
// Try to load cached index
|
|
456
|
+
if (!force) {
|
|
457
|
+
const cached = loadCachedIndex(absDir);
|
|
458
|
+
if (cached) {
|
|
459
|
+
// Validate cache by checking a sample of files
|
|
460
|
+
const sampleFiles = Object.keys(cached.files).slice(0, 10);
|
|
461
|
+
let cacheValid = true;
|
|
462
|
+
for (const file of sampleFiles) {
|
|
463
|
+
const fullPath = path.join(absDir, file);
|
|
464
|
+
if (!fs.existsSync(fullPath)) {
|
|
465
|
+
cacheValid = false;
|
|
466
|
+
break;
|
|
467
|
+
}
|
|
468
|
+
const stats = fs.statSync(fullPath);
|
|
469
|
+
if (stats.mtimeMs > cached.files[file].mtime) {
|
|
470
|
+
cacheValid = false;
|
|
471
|
+
break;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
if (cacheValid) {
|
|
475
|
+
if (verbose) {
|
|
476
|
+
console.log('[Index] Using cached structural index');
|
|
477
|
+
}
|
|
478
|
+
return cached;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
if (verbose) {
|
|
483
|
+
console.log('[Index] Building structural index...');
|
|
484
|
+
}
|
|
485
|
+
const fileIndex = {};
|
|
486
|
+
const languages = new Set();
|
|
487
|
+
let totalSize = 0;
|
|
488
|
+
// Walk directory
|
|
489
|
+
function walk(dir) {
|
|
490
|
+
try {
|
|
491
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
492
|
+
for (const entry of entries) {
|
|
493
|
+
const fullPath = path.join(dir, entry.name);
|
|
494
|
+
const relativePath = path.relative(absDir, fullPath);
|
|
495
|
+
if (entry.isDirectory()) {
|
|
496
|
+
if (!IGNORE_DIRS.includes(entry.name)) {
|
|
497
|
+
walk(fullPath);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
else if (entry.isFile() && shouldIncludeFile(relativePath, entry.name)) {
|
|
501
|
+
try {
|
|
502
|
+
const stats = fs.statSync(fullPath);
|
|
503
|
+
const content = fs.readFileSync(fullPath, 'utf-8');
|
|
504
|
+
const ext = path.extname(entry.name);
|
|
505
|
+
const lang = getLanguage(ext);
|
|
506
|
+
if (lang !== 'unknown') {
|
|
507
|
+
languages.add(lang);
|
|
508
|
+
}
|
|
509
|
+
const isLarge = needsChunking(stats.size, maxFileSize);
|
|
510
|
+
fileIndex[relativePath] = {
|
|
511
|
+
path: relativePath,
|
|
512
|
+
hash: hashContent(content),
|
|
513
|
+
size: stats.size,
|
|
514
|
+
mtime: stats.mtimeMs,
|
|
515
|
+
imports: extractImports(content, ext),
|
|
516
|
+
exports: extractExports(content, ext),
|
|
517
|
+
extension: ext,
|
|
518
|
+
chunked: isLarge,
|
|
519
|
+
chunkCount: isLarge ? chunkFile(content, chunkSize).length : undefined,
|
|
520
|
+
};
|
|
521
|
+
totalSize += stats.size;
|
|
522
|
+
}
|
|
523
|
+
catch {
|
|
524
|
+
// Skip unreadable files
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
catch {
|
|
530
|
+
// Skip unreadable directories
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
walk(absDir);
|
|
534
|
+
// Build dependency graph
|
|
535
|
+
const dependencies = buildDeps ? buildDependencyGraph(fileIndex) : [];
|
|
536
|
+
// Build clusters
|
|
537
|
+
const clusters = enableClustering ? buildClusters(fileIndex, dependencies) : [];
|
|
538
|
+
// Detect frameworks and package manager
|
|
539
|
+
const metadata = {
|
|
540
|
+
fileCount: Object.keys(fileIndex).length,
|
|
541
|
+
totalSize,
|
|
542
|
+
languages: Array.from(languages),
|
|
543
|
+
frameworks: detectFrameworks(fileIndex),
|
|
544
|
+
packageManager: detectPackageManager(absDir),
|
|
545
|
+
};
|
|
546
|
+
const index = {
|
|
547
|
+
version: INDEX_VERSION,
|
|
548
|
+
projectRoot: absDir,
|
|
549
|
+
createdAt: Date.now(),
|
|
550
|
+
updatedAt: Date.now(),
|
|
551
|
+
files: fileIndex,
|
|
552
|
+
dependencies,
|
|
553
|
+
clusters,
|
|
554
|
+
metadata,
|
|
555
|
+
};
|
|
556
|
+
// Save to cache
|
|
557
|
+
saveIndexToCache(index);
|
|
558
|
+
if (verbose) {
|
|
559
|
+
console.log(`[Index] Indexed ${metadata.fileCount} files, ${clusters.length} clusters`);
|
|
560
|
+
}
|
|
561
|
+
return index;
|
|
562
|
+
}
|
|
563
|
+
/**
|
|
564
|
+
* Detect frameworks from file index
|
|
565
|
+
*/
|
|
566
|
+
function detectFrameworks(fileIndex) {
|
|
567
|
+
const frameworks = new Set();
|
|
568
|
+
const files = Object.keys(fileIndex);
|
|
569
|
+
const patterns = [
|
|
570
|
+
[/next\.config/, 'Next.js'],
|
|
571
|
+
[/nuxt\.config/, 'Nuxt'],
|
|
572
|
+
[/angular\.json/, 'Angular'],
|
|
573
|
+
[/vue\.config|\.vue$/, 'Vue'],
|
|
574
|
+
[/svelte\.config|\.svelte$/, 'Svelte'],
|
|
575
|
+
[/astro\.config|\.astro$/, 'Astro'],
|
|
576
|
+
[/remix\.config/, 'Remix'],
|
|
577
|
+
[/express/, 'Express'],
|
|
578
|
+
[/fastify/, 'Fastify'],
|
|
579
|
+
[/nest-cli\.json/, 'NestJS'],
|
|
580
|
+
[/django|wsgi\.py/, 'Django'],
|
|
581
|
+
[/flask/, 'Flask'],
|
|
582
|
+
[/fastapi/, 'FastAPI'],
|
|
583
|
+
[/rails/, 'Rails'],
|
|
584
|
+
[/spring/, 'Spring'],
|
|
585
|
+
[/\.prisma$/, 'Prisma'],
|
|
586
|
+
];
|
|
587
|
+
for (const file of files) {
|
|
588
|
+
for (const [pattern, name] of patterns) {
|
|
589
|
+
if (pattern.test(file)) {
|
|
590
|
+
frameworks.add(name);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
return Array.from(frameworks);
|
|
595
|
+
}
|
|
596
|
+
/**
|
|
597
|
+
* Detect package manager
|
|
598
|
+
*/
|
|
599
|
+
function detectPackageManager(dir) {
|
|
600
|
+
if (fs.existsSync(path.join(dir, 'pnpm-lock.yaml')))
|
|
601
|
+
return 'pnpm';
|
|
602
|
+
if (fs.existsSync(path.join(dir, 'yarn.lock')))
|
|
603
|
+
return 'yarn';
|
|
604
|
+
if (fs.existsSync(path.join(dir, 'package-lock.json')))
|
|
605
|
+
return 'npm';
|
|
606
|
+
if (fs.existsSync(path.join(dir, 'Cargo.toml')))
|
|
607
|
+
return 'cargo';
|
|
608
|
+
if (fs.existsSync(path.join(dir, 'go.mod')))
|
|
609
|
+
return 'go';
|
|
610
|
+
if (fs.existsSync(path.join(dir, 'requirements.txt')) || fs.existsSync(path.join(dir, 'pyproject.toml')))
|
|
611
|
+
return 'pip';
|
|
612
|
+
if (fs.existsSync(path.join(dir, 'pom.xml')))
|
|
613
|
+
return 'maven';
|
|
614
|
+
if (fs.existsSync(path.join(dir, 'build.gradle')) || fs.existsSync(path.join(dir, 'build.gradle.kts')))
|
|
615
|
+
return 'gradle';
|
|
616
|
+
return undefined;
|
|
617
|
+
}
|
|
618
|
+
// ============================================================================
|
|
619
|
+
// Incremental Update
|
|
620
|
+
// ============================================================================
|
|
621
|
+
/**
|
|
622
|
+
* Update index with changed files only
|
|
623
|
+
*/
|
|
624
|
+
export async function updateStructuralIndex(directory, options = {}) {
|
|
625
|
+
const absDir = path.resolve(directory);
|
|
626
|
+
const cached = loadCachedIndex(absDir);
|
|
627
|
+
if (!cached) {
|
|
628
|
+
const index = await buildStructuralIndex(directory, options);
|
|
629
|
+
return { index, changedFiles: Object.keys(index.files) };
|
|
630
|
+
}
|
|
631
|
+
const changedFiles = [];
|
|
632
|
+
const { maxFileSize = DEFAULT_MAX_FILE_SIZE, chunkSize = DEFAULT_CHUNK_SIZE } = options;
|
|
633
|
+
// Check each cached file for changes
|
|
634
|
+
for (const [relativePath, entry] of Object.entries(cached.files)) {
|
|
635
|
+
const fullPath = path.join(absDir, relativePath);
|
|
636
|
+
if (!fs.existsSync(fullPath)) {
|
|
637
|
+
// File deleted
|
|
638
|
+
delete cached.files[relativePath];
|
|
639
|
+
changedFiles.push(relativePath);
|
|
640
|
+
}
|
|
641
|
+
else {
|
|
642
|
+
const stats = fs.statSync(fullPath);
|
|
643
|
+
if (stats.mtimeMs > entry.mtime) {
|
|
644
|
+
// File modified
|
|
645
|
+
const content = fs.readFileSync(fullPath, 'utf-8');
|
|
646
|
+
const ext = path.extname(relativePath);
|
|
647
|
+
const isLarge = needsChunking(stats.size, maxFileSize);
|
|
648
|
+
cached.files[relativePath] = {
|
|
649
|
+
...entry,
|
|
650
|
+
hash: hashContent(content),
|
|
651
|
+
size: stats.size,
|
|
652
|
+
mtime: stats.mtimeMs,
|
|
653
|
+
imports: extractImports(content, ext),
|
|
654
|
+
exports: extractExports(content, ext),
|
|
655
|
+
chunked: isLarge,
|
|
656
|
+
chunkCount: isLarge ? chunkFile(content, chunkSize).length : undefined,
|
|
657
|
+
};
|
|
658
|
+
changedFiles.push(relativePath);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
// Rebuild dependency graph if files changed
|
|
663
|
+
if (changedFiles.length > 0) {
|
|
664
|
+
cached.dependencies = buildDependencyGraph(cached.files);
|
|
665
|
+
cached.clusters = buildClusters(cached.files, cached.dependencies);
|
|
666
|
+
cached.updatedAt = Date.now();
|
|
667
|
+
cached.metadata.fileCount = Object.keys(cached.files).length;
|
|
668
|
+
saveIndexToCache(cached);
|
|
669
|
+
}
|
|
670
|
+
return { index: cached, changedFiles };
|
|
671
|
+
}
|
|
672
|
+
// ============================================================================
|
|
673
|
+
// Query Helpers
|
|
674
|
+
// ============================================================================
|
|
675
|
+
/**
|
|
676
|
+
* Get files that depend on a given file
|
|
677
|
+
*/
|
|
678
|
+
export function getDependents(index, filePath) {
|
|
679
|
+
return index.dependencies
|
|
680
|
+
.filter(e => e.to === filePath)
|
|
681
|
+
.map(e => e.from);
|
|
682
|
+
}
|
|
683
|
+
/**
|
|
684
|
+
* Get files that a given file depends on
|
|
685
|
+
*/
|
|
686
|
+
export function getDependencies(index, filePath) {
|
|
687
|
+
return index.dependencies
|
|
688
|
+
.filter(e => e.from === filePath)
|
|
689
|
+
.map(e => e.to);
|
|
690
|
+
}
|
|
691
|
+
/**
|
|
692
|
+
* Get the cluster containing a file
|
|
693
|
+
*/
|
|
694
|
+
export function getFileCluster(index, filePath) {
|
|
695
|
+
return index.clusters.find(c => c.files.includes(filePath));
|
|
696
|
+
}
|
|
697
|
+
/**
|
|
698
|
+
* Get files ordered by analysis priority (entry points first)
|
|
699
|
+
*/
|
|
700
|
+
export function getAnalysisPriority(index) {
|
|
701
|
+
const priority = [];
|
|
702
|
+
const added = new Set();
|
|
703
|
+
// Add cluster entry points first
|
|
704
|
+
for (const cluster of index.clusters) {
|
|
705
|
+
for (const entry of cluster.entryPoints) {
|
|
706
|
+
if (!added.has(entry)) {
|
|
707
|
+
priority.push(entry);
|
|
708
|
+
added.add(entry);
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
// Add remaining files
|
|
713
|
+
for (const file of Object.keys(index.files)) {
|
|
714
|
+
if (!added.has(file)) {
|
|
715
|
+
priority.push(file);
|
|
716
|
+
added.add(file);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
return priority;
|
|
720
|
+
}
|
|
721
|
+
//# sourceMappingURL=structural-index.js.map
|