archicore 0.3.5 → 0.3.7
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/cli/commands/interactive.js +220 -1
- package/dist/github/github-service.d.ts +4 -4
- package/dist/github/github-service.js +29 -13
- package/dist/gitlab/gitlab-service.js +48 -15
- package/dist/server/index.js +68 -1
- package/dist/server/routes/api.js +242 -2
- package/dist/server/routes/auth.d.ts +1 -0
- package/dist/server/routes/auth.js +11 -3
- package/dist/server/routes/gitlab.js +18 -46
- package/dist/server/routes/report-issue.js +103 -0
- package/dist/server/routes/upload.js +3 -3
- package/dist/server/services/encryption.js +20 -4
- package/dist/server/services/enterprise-indexer.d.ts +116 -0
- package/dist/server/services/enterprise-indexer.js +529 -0
- package/package.json +2 -1
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Enterprise Indexer
|
|
3
|
+
*
|
|
4
|
+
* Handles large-scale projects (50K+ files) with intelligent sampling,
|
|
5
|
+
* incremental indexing, and tiered analysis modes.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Enterprise indexing options
|
|
9
|
+
*/
|
|
10
|
+
export interface EnterpriseIndexingOptions {
|
|
11
|
+
tier: 'quick' | 'standard' | 'deep';
|
|
12
|
+
sampling: {
|
|
13
|
+
enabled: boolean;
|
|
14
|
+
maxFiles: number;
|
|
15
|
+
strategy: 'hot-files' | 'random' | 'directory-balanced' | 'smart';
|
|
16
|
+
};
|
|
17
|
+
incremental: {
|
|
18
|
+
enabled: boolean;
|
|
19
|
+
since?: string;
|
|
20
|
+
};
|
|
21
|
+
focusDirectories?: string[];
|
|
22
|
+
excludePatterns?: string[];
|
|
23
|
+
memoryLimitMB?: number;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Analysis tier configuration
|
|
27
|
+
*/
|
|
28
|
+
declare const TIER_CONFIG: {
|
|
29
|
+
quick: {
|
|
30
|
+
maxFiles: number;
|
|
31
|
+
skipEmbeddings: boolean;
|
|
32
|
+
skipSecurity: boolean;
|
|
33
|
+
skipDuplication: boolean;
|
|
34
|
+
description: string;
|
|
35
|
+
};
|
|
36
|
+
standard: {
|
|
37
|
+
maxFiles: number;
|
|
38
|
+
skipEmbeddings: boolean;
|
|
39
|
+
skipSecurity: boolean;
|
|
40
|
+
skipDuplication: boolean;
|
|
41
|
+
description: string;
|
|
42
|
+
};
|
|
43
|
+
deep: {
|
|
44
|
+
maxFiles: number;
|
|
45
|
+
skipEmbeddings: boolean;
|
|
46
|
+
skipSecurity: boolean;
|
|
47
|
+
skipDuplication: boolean;
|
|
48
|
+
description: string;
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* Default options for enterprise indexing
|
|
53
|
+
*/
|
|
54
|
+
export declare const DEFAULT_ENTERPRISE_OPTIONS: EnterpriseIndexingOptions;
|
|
55
|
+
export declare class EnterpriseIndexer {
|
|
56
|
+
private projectPath;
|
|
57
|
+
private options;
|
|
58
|
+
private gitAvailable;
|
|
59
|
+
constructor(projectPath: string, options?: Partial<EnterpriseIndexingOptions>);
|
|
60
|
+
/**
|
|
61
|
+
* Initialize - check git availability
|
|
62
|
+
*/
|
|
63
|
+
initialize(): Promise<void>;
|
|
64
|
+
/**
|
|
65
|
+
* Get project size estimation
|
|
66
|
+
*/
|
|
67
|
+
getProjectSize(): Promise<{
|
|
68
|
+
totalFiles: number;
|
|
69
|
+
totalSizeMB: number;
|
|
70
|
+
languageDistribution: Record<string, number>;
|
|
71
|
+
recommendation: 'quick' | 'standard' | 'deep';
|
|
72
|
+
estimatedTimeMinutes: number;
|
|
73
|
+
}>;
|
|
74
|
+
/**
|
|
75
|
+
* Get files to index based on sampling strategy
|
|
76
|
+
*/
|
|
77
|
+
getFilesToIndex(): Promise<string[]>;
|
|
78
|
+
/**
|
|
79
|
+
* Get files changed since last index (for incremental indexing)
|
|
80
|
+
*/
|
|
81
|
+
getChangedFiles(since: string): Promise<string[]>;
|
|
82
|
+
/**
|
|
83
|
+
* Scan all files in project
|
|
84
|
+
*/
|
|
85
|
+
private scanAllFiles;
|
|
86
|
+
/**
|
|
87
|
+
* Select hot files (most frequently changed)
|
|
88
|
+
*/
|
|
89
|
+
private selectHotFiles;
|
|
90
|
+
/**
|
|
91
|
+
* Random selection
|
|
92
|
+
*/
|
|
93
|
+
private selectRandom;
|
|
94
|
+
/**
|
|
95
|
+
* Directory-balanced selection
|
|
96
|
+
*/
|
|
97
|
+
private selectDirectoryBalanced;
|
|
98
|
+
/**
|
|
99
|
+
* Smart selection - combines multiple heuristics
|
|
100
|
+
*/
|
|
101
|
+
private selectSmart;
|
|
102
|
+
/**
|
|
103
|
+
* Get import counts for files
|
|
104
|
+
*/
|
|
105
|
+
private getImportCounts;
|
|
106
|
+
/**
|
|
107
|
+
* Get commit counts from git
|
|
108
|
+
*/
|
|
109
|
+
private getCommitCounts;
|
|
110
|
+
/**
|
|
111
|
+
* Get tier configuration
|
|
112
|
+
*/
|
|
113
|
+
getTierConfig(): typeof TIER_CONFIG[keyof typeof TIER_CONFIG];
|
|
114
|
+
}
|
|
115
|
+
export { TIER_CONFIG };
|
|
116
|
+
//# sourceMappingURL=enterprise-indexer.d.ts.map
|
|
@@ -0,0 +1,529 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Enterprise Indexer
|
|
3
|
+
*
|
|
4
|
+
* Handles large-scale projects (50K+ files) with intelligent sampling,
|
|
5
|
+
* incremental indexing, and tiered analysis modes.
|
|
6
|
+
*/
|
|
7
|
+
import { exec } from 'child_process';
|
|
8
|
+
import { promisify } from 'util';
|
|
9
|
+
import { existsSync, statSync } from 'fs';
|
|
10
|
+
import { readdir, stat, readFile } from 'fs/promises';
|
|
11
|
+
import path from 'path';
|
|
12
|
+
import { Logger } from '../../utils/logger.js';
|
|
13
|
+
const execAsync = promisify(exec);
|
|
14
|
+
/**
|
|
15
|
+
* Analysis tier configuration
|
|
16
|
+
*/
|
|
17
|
+
const TIER_CONFIG = {
|
|
18
|
+
quick: {
|
|
19
|
+
maxFiles: 1000,
|
|
20
|
+
skipEmbeddings: true,
|
|
21
|
+
skipSecurity: true,
|
|
22
|
+
skipDuplication: true,
|
|
23
|
+
description: 'Fast overview - structure and basic metrics only'
|
|
24
|
+
},
|
|
25
|
+
standard: {
|
|
26
|
+
maxFiles: 5000,
|
|
27
|
+
skipEmbeddings: false,
|
|
28
|
+
skipSecurity: false,
|
|
29
|
+
skipDuplication: false,
|
|
30
|
+
description: 'Standard analysis - all features with sampling'
|
|
31
|
+
},
|
|
32
|
+
deep: {
|
|
33
|
+
maxFiles: 50000,
|
|
34
|
+
skipEmbeddings: false,
|
|
35
|
+
skipSecurity: false,
|
|
36
|
+
skipDuplication: false,
|
|
37
|
+
description: 'Deep analysis - comprehensive analysis for entire project'
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* Default options for enterprise indexing
|
|
42
|
+
*/
|
|
43
|
+
export const DEFAULT_ENTERPRISE_OPTIONS = {
|
|
44
|
+
tier: 'standard',
|
|
45
|
+
sampling: {
|
|
46
|
+
enabled: true,
|
|
47
|
+
maxFiles: 5000,
|
|
48
|
+
strategy: 'smart'
|
|
49
|
+
},
|
|
50
|
+
incremental: {
|
|
51
|
+
enabled: false
|
|
52
|
+
},
|
|
53
|
+
excludePatterns: [
|
|
54
|
+
'node_modules',
|
|
55
|
+
'.git',
|
|
56
|
+
'dist',
|
|
57
|
+
'build',
|
|
58
|
+
'__pycache__',
|
|
59
|
+
'.cache',
|
|
60
|
+
'vendor',
|
|
61
|
+
'coverage',
|
|
62
|
+
'.next',
|
|
63
|
+
'.nuxt'
|
|
64
|
+
],
|
|
65
|
+
memoryLimitMB: 2048
|
|
66
|
+
};
|
|
67
|
+
export class EnterpriseIndexer {
|
|
68
|
+
projectPath;
|
|
69
|
+
options;
|
|
70
|
+
gitAvailable = false;
|
|
71
|
+
constructor(projectPath, options = {}) {
|
|
72
|
+
this.projectPath = projectPath;
|
|
73
|
+
this.options = { ...DEFAULT_ENTERPRISE_OPTIONS, ...options };
|
|
74
|
+
// Apply tier config
|
|
75
|
+
const tierConfig = TIER_CONFIG[this.options.tier];
|
|
76
|
+
if (this.options.sampling.enabled && !options.sampling?.maxFiles) {
|
|
77
|
+
this.options.sampling.maxFiles = tierConfig.maxFiles;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Initialize - check git availability
|
|
82
|
+
*/
|
|
83
|
+
async initialize() {
|
|
84
|
+
try {
|
|
85
|
+
await execAsync('git --version', { cwd: this.projectPath });
|
|
86
|
+
const gitDir = path.join(this.projectPath, '.git');
|
|
87
|
+
this.gitAvailable = existsSync(gitDir);
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
this.gitAvailable = false;
|
|
91
|
+
}
|
|
92
|
+
Logger.info(`Enterprise indexer initialized. Git available: ${this.gitAvailable}`);
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Get project size estimation
|
|
96
|
+
*/
|
|
97
|
+
async getProjectSize() {
|
|
98
|
+
const files = await this.scanAllFiles();
|
|
99
|
+
const totalSize = files.reduce((sum, f) => sum + (f.size || 0), 0);
|
|
100
|
+
// Count by language
|
|
101
|
+
const languageDistribution = {};
|
|
102
|
+
for (const file of files) {
|
|
103
|
+
const ext = path.extname(file.path).toLowerCase() || '.other';
|
|
104
|
+
languageDistribution[ext] = (languageDistribution[ext] || 0) + 1;
|
|
105
|
+
}
|
|
106
|
+
// Recommendation based on size
|
|
107
|
+
let recommendation;
|
|
108
|
+
if (files.length < 1000) {
|
|
109
|
+
recommendation = 'deep';
|
|
110
|
+
}
|
|
111
|
+
else if (files.length < 10000) {
|
|
112
|
+
recommendation = 'standard';
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
recommendation = 'quick';
|
|
116
|
+
}
|
|
117
|
+
// Estimate time (rough)
|
|
118
|
+
// Quick: ~10 files/sec, Standard: ~5 files/sec, Deep: ~2 files/sec
|
|
119
|
+
const filesPerSecond = recommendation === 'quick' ? 10 : recommendation === 'standard' ? 5 : 2;
|
|
120
|
+
const filesToProcess = Math.min(files.length, TIER_CONFIG[recommendation].maxFiles);
|
|
121
|
+
const estimatedTimeMinutes = Math.ceil(filesToProcess / filesPerSecond / 60);
|
|
122
|
+
return {
|
|
123
|
+
totalFiles: files.length,
|
|
124
|
+
totalSizeMB: Math.round(totalSize / (1024 * 1024) * 100) / 100,
|
|
125
|
+
languageDistribution,
|
|
126
|
+
recommendation,
|
|
127
|
+
estimatedTimeMinutes
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Get files to index based on sampling strategy
|
|
132
|
+
*/
|
|
133
|
+
async getFilesToIndex() {
|
|
134
|
+
const allFiles = await this.scanAllFiles();
|
|
135
|
+
Logger.info(`Total files found: ${allFiles.length}`);
|
|
136
|
+
if (!this.options.sampling.enabled) {
|
|
137
|
+
return allFiles.map(f => f.path);
|
|
138
|
+
}
|
|
139
|
+
const maxFiles = this.options.sampling.maxFiles;
|
|
140
|
+
if (allFiles.length <= maxFiles) {
|
|
141
|
+
Logger.info(`All files within limit (${allFiles.length} <= ${maxFiles})`);
|
|
142
|
+
return allFiles.map(f => f.path);
|
|
143
|
+
}
|
|
144
|
+
// Apply sampling strategy
|
|
145
|
+
let selectedFiles;
|
|
146
|
+
switch (this.options.sampling.strategy) {
|
|
147
|
+
case 'hot-files':
|
|
148
|
+
selectedFiles = await this.selectHotFiles(allFiles, maxFiles);
|
|
149
|
+
break;
|
|
150
|
+
case 'random':
|
|
151
|
+
selectedFiles = this.selectRandom(allFiles, maxFiles);
|
|
152
|
+
break;
|
|
153
|
+
case 'directory-balanced':
|
|
154
|
+
selectedFiles = this.selectDirectoryBalanced(allFiles, maxFiles);
|
|
155
|
+
break;
|
|
156
|
+
case 'smart':
|
|
157
|
+
default:
|
|
158
|
+
selectedFiles = await this.selectSmart(allFiles, maxFiles);
|
|
159
|
+
}
|
|
160
|
+
Logger.info(`Selected ${selectedFiles.length} files using '${this.options.sampling.strategy}' strategy`);
|
|
161
|
+
return selectedFiles;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Get files changed since last index (for incremental indexing)
|
|
165
|
+
*/
|
|
166
|
+
async getChangedFiles(since) {
|
|
167
|
+
if (!this.gitAvailable) {
|
|
168
|
+
Logger.warn('Git not available - cannot determine changed files');
|
|
169
|
+
return [];
|
|
170
|
+
}
|
|
171
|
+
try {
|
|
172
|
+
// Try to use git to get changed files
|
|
173
|
+
let command;
|
|
174
|
+
if (since.match(/^[0-9a-f]{7,40}$/i)) {
|
|
175
|
+
// Git commit hash
|
|
176
|
+
command = `git diff --name-only ${since}..HEAD`;
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
// ISO date
|
|
180
|
+
command = `git log --since="${since}" --name-only --pretty=format: | sort -u`;
|
|
181
|
+
}
|
|
182
|
+
const { stdout } = await execAsync(command, { cwd: this.projectPath });
|
|
183
|
+
const changedFiles = stdout.split('\n')
|
|
184
|
+
.map(f => f.trim())
|
|
185
|
+
.filter(f => f.length > 0)
|
|
186
|
+
.map(f => path.join(this.projectPath, f))
|
|
187
|
+
.filter(f => existsSync(f));
|
|
188
|
+
Logger.info(`Found ${changedFiles.length} changed files since ${since}`);
|
|
189
|
+
return changedFiles;
|
|
190
|
+
}
|
|
191
|
+
catch (error) {
|
|
192
|
+
Logger.error('Failed to get changed files:', error);
|
|
193
|
+
return [];
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Scan all files in project
|
|
198
|
+
*/
|
|
199
|
+
async scanAllFiles() {
|
|
200
|
+
const files = [];
|
|
201
|
+
const excludePatterns = this.options.excludePatterns || [];
|
|
202
|
+
const CODE_EXTENSIONS = new Set([
|
|
203
|
+
'.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs',
|
|
204
|
+
'.py', '.rb', '.go', '.rs', '.java', '.kt', '.scala',
|
|
205
|
+
'.c', '.cpp', '.h', '.hpp', '.cs', '.swift',
|
|
206
|
+
'.vue', '.svelte', '.astro',
|
|
207
|
+
'.php', '.pl', '.pm', '.lua', '.r',
|
|
208
|
+
'.sql', '.graphql', '.gql',
|
|
209
|
+
'.yaml', '.yml', '.json', '.toml', '.xml'
|
|
210
|
+
]);
|
|
211
|
+
const scanDir = async (dir) => {
|
|
212
|
+
try {
|
|
213
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
214
|
+
for (const entry of entries) {
|
|
215
|
+
const fullPath = path.join(dir, entry.name);
|
|
216
|
+
// Check exclude patterns
|
|
217
|
+
if (excludePatterns.some(p => fullPath.includes(p))) {
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
if (entry.isDirectory()) {
|
|
221
|
+
await scanDir(fullPath);
|
|
222
|
+
}
|
|
223
|
+
else if (entry.isFile()) {
|
|
224
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
225
|
+
if (CODE_EXTENSIONS.has(ext)) {
|
|
226
|
+
try {
|
|
227
|
+
const stats = await stat(fullPath);
|
|
228
|
+
// Skip very large files (> 1MB)
|
|
229
|
+
if (stats.size < 1024 * 1024) {
|
|
230
|
+
files.push({ path: fullPath, size: stats.size });
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
catch {
|
|
234
|
+
// Skip files we can't stat
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
catch {
|
|
241
|
+
// Skip directories we can't read
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
await scanDir(this.projectPath);
|
|
245
|
+
return files;
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Select hot files (most frequently changed)
|
|
249
|
+
*/
|
|
250
|
+
async selectHotFiles(files, maxFiles) {
|
|
251
|
+
if (!this.gitAvailable) {
|
|
252
|
+
Logger.warn('Git not available, falling back to smart selection');
|
|
253
|
+
return this.selectSmart(files, maxFiles);
|
|
254
|
+
}
|
|
255
|
+
const fileScores = [];
|
|
256
|
+
// Get commit counts for each file
|
|
257
|
+
try {
|
|
258
|
+
const { stdout } = await execAsync('git log --name-only --pretty=format: | sort | uniq -c | sort -rn | head -5000', { cwd: this.projectPath, maxBuffer: 10 * 1024 * 1024 });
|
|
259
|
+
const commitCounts = new Map();
|
|
260
|
+
for (const line of stdout.split('\n')) {
|
|
261
|
+
const match = line.trim().match(/^\s*(\d+)\s+(.+)$/);
|
|
262
|
+
if (match) {
|
|
263
|
+
const count = parseInt(match[1], 10);
|
|
264
|
+
const filePath = path.join(this.projectPath, match[2]);
|
|
265
|
+
commitCounts.set(filePath, count);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
for (const file of files) {
|
|
269
|
+
const commitCount = commitCounts.get(file.path) || 0;
|
|
270
|
+
fileScores.push({
|
|
271
|
+
path: file.path,
|
|
272
|
+
score: commitCount,
|
|
273
|
+
reasons: [`${commitCount} commits`],
|
|
274
|
+
lastModified: new Date(),
|
|
275
|
+
commitCount
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
catch {
|
|
280
|
+
// Fallback to modification time
|
|
281
|
+
for (const file of files) {
|
|
282
|
+
try {
|
|
283
|
+
const stats = statSync(file.path);
|
|
284
|
+
fileScores.push({
|
|
285
|
+
path: file.path,
|
|
286
|
+
score: stats.mtimeMs,
|
|
287
|
+
reasons: ['recent modification'],
|
|
288
|
+
lastModified: stats.mtime
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
catch {
|
|
292
|
+
fileScores.push({
|
|
293
|
+
path: file.path,
|
|
294
|
+
score: 0,
|
|
295
|
+
reasons: [],
|
|
296
|
+
lastModified: new Date(0)
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
// Sort by score and select top N
|
|
302
|
+
fileScores.sort((a, b) => b.score - a.score);
|
|
303
|
+
return fileScores.slice(0, maxFiles).map(f => f.path);
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Random selection
|
|
307
|
+
*/
|
|
308
|
+
selectRandom(files, maxFiles) {
|
|
309
|
+
const shuffled = [...files].sort(() => Math.random() - 0.5);
|
|
310
|
+
return shuffled.slice(0, maxFiles).map(f => f.path);
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Directory-balanced selection
|
|
314
|
+
*/
|
|
315
|
+
selectDirectoryBalanced(files, maxFiles) {
|
|
316
|
+
// Group by top-level directory
|
|
317
|
+
const byDirectory = new Map();
|
|
318
|
+
for (const file of files) {
|
|
319
|
+
const relativePath = path.relative(this.projectPath, file.path);
|
|
320
|
+
const topDir = relativePath.split(path.sep)[0] || '.';
|
|
321
|
+
if (!byDirectory.has(topDir)) {
|
|
322
|
+
byDirectory.set(topDir, []);
|
|
323
|
+
}
|
|
324
|
+
byDirectory.get(topDir).push(file);
|
|
325
|
+
}
|
|
326
|
+
// Calculate files per directory
|
|
327
|
+
const dirCount = byDirectory.size;
|
|
328
|
+
const filesPerDir = Math.ceil(maxFiles / dirCount);
|
|
329
|
+
const selected = [];
|
|
330
|
+
for (const [, dirFiles] of byDirectory) {
|
|
331
|
+
// Sort by recency (assume larger files are more important)
|
|
332
|
+
dirFiles.sort((a, b) => b.size - a.size);
|
|
333
|
+
const toTake = Math.min(filesPerDir, dirFiles.length);
|
|
334
|
+
selected.push(...dirFiles.slice(0, toTake).map(f => f.path));
|
|
335
|
+
}
|
|
336
|
+
// If we have room, add more from the largest directories
|
|
337
|
+
if (selected.length < maxFiles) {
|
|
338
|
+
const remaining = maxFiles - selected.length;
|
|
339
|
+
const allRemaining = files
|
|
340
|
+
.filter(f => !selected.includes(f.path))
|
|
341
|
+
.sort((a, b) => b.size - a.size)
|
|
342
|
+
.slice(0, remaining);
|
|
343
|
+
selected.push(...allRemaining.map(f => f.path));
|
|
344
|
+
}
|
|
345
|
+
return selected.slice(0, maxFiles);
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* Smart selection - combines multiple heuristics
|
|
349
|
+
*/
|
|
350
|
+
async selectSmart(files, maxFiles) {
|
|
351
|
+
const fileScores = [];
|
|
352
|
+
// Priority patterns (entry points, configs, core logic)
|
|
353
|
+
const HIGH_PRIORITY_PATTERNS = [
|
|
354
|
+
/index\.(ts|js|tsx|jsx)$/,
|
|
355
|
+
/main\.(ts|js|py|go|rs)$/,
|
|
356
|
+
/app\.(ts|js|tsx|jsx|vue)$/,
|
|
357
|
+
/server\.(ts|js)$/,
|
|
358
|
+
/config\.(ts|js|json|yaml|yml)$/,
|
|
359
|
+
/routes?\.(ts|js)$/,
|
|
360
|
+
/api\.(ts|js)$/,
|
|
361
|
+
/service\.(ts|js)$/,
|
|
362
|
+
/controller\.(ts|js)$/,
|
|
363
|
+
/model\.(ts|js|py)$/,
|
|
364
|
+
/schema\.(ts|js|graphql)$/,
|
|
365
|
+
/types?\.(ts|d\.ts)$/,
|
|
366
|
+
/package\.json$/,
|
|
367
|
+
/tsconfig\.json$/,
|
|
368
|
+
/\.env\.example$/
|
|
369
|
+
];
|
|
370
|
+
// Get import counts if possible
|
|
371
|
+
const importCounts = await this.getImportCounts(files);
|
|
372
|
+
// Get commit counts if git available
|
|
373
|
+
let commitCounts = new Map();
|
|
374
|
+
if (this.gitAvailable) {
|
|
375
|
+
commitCounts = await this.getCommitCounts();
|
|
376
|
+
}
|
|
377
|
+
for (const file of files) {
|
|
378
|
+
const relativePath = path.relative(this.projectPath, file.path);
|
|
379
|
+
let score = 0;
|
|
380
|
+
const reasons = [];
|
|
381
|
+
// Check priority patterns
|
|
382
|
+
for (const pattern of HIGH_PRIORITY_PATTERNS) {
|
|
383
|
+
if (pattern.test(relativePath)) {
|
|
384
|
+
score += 100;
|
|
385
|
+
reasons.push('priority file');
|
|
386
|
+
break;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
// Focus directories get higher priority
|
|
390
|
+
if (this.options.focusDirectories) {
|
|
391
|
+
for (const focusDir of this.options.focusDirectories) {
|
|
392
|
+
if (relativePath.startsWith(focusDir)) {
|
|
393
|
+
score += 50;
|
|
394
|
+
reasons.push('focus directory');
|
|
395
|
+
break;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
// Bonus for shallow depth (closer to root = more important)
|
|
400
|
+
const depth = relativePath.split(path.sep).length;
|
|
401
|
+
score += Math.max(0, 20 - depth * 2);
|
|
402
|
+
// Bonus for import count
|
|
403
|
+
const imports = importCounts.get(file.path) || 0;
|
|
404
|
+
if (imports > 0) {
|
|
405
|
+
score += Math.min(imports * 5, 50);
|
|
406
|
+
reasons.push(`${imports} imports`);
|
|
407
|
+
}
|
|
408
|
+
// Bonus for commit count
|
|
409
|
+
const commits = commitCounts.get(file.path) || 0;
|
|
410
|
+
if (commits > 0) {
|
|
411
|
+
score += Math.min(commits * 2, 30);
|
|
412
|
+
reasons.push(`${commits} commits`);
|
|
413
|
+
}
|
|
414
|
+
// Penalty for test files (still include some)
|
|
415
|
+
if (relativePath.includes('test') || relativePath.includes('spec') || relativePath.includes('__tests__')) {
|
|
416
|
+
score -= 30;
|
|
417
|
+
}
|
|
418
|
+
// Penalty for generated files
|
|
419
|
+
if (relativePath.includes('.generated') || relativePath.includes('.d.ts')) {
|
|
420
|
+
score -= 40;
|
|
421
|
+
}
|
|
422
|
+
fileScores.push({
|
|
423
|
+
path: file.path,
|
|
424
|
+
score,
|
|
425
|
+
reasons,
|
|
426
|
+
lastModified: new Date(),
|
|
427
|
+
commitCount: commits,
|
|
428
|
+
importCount: imports
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
// Sort by score
|
|
432
|
+
fileScores.sort((a, b) => b.score - a.score);
|
|
433
|
+
// Log top 10 for debugging
|
|
434
|
+
Logger.debug('Top 10 files by score:');
|
|
435
|
+
for (const f of fileScores.slice(0, 10)) {
|
|
436
|
+
Logger.debug(` ${f.score}: ${path.relative(this.projectPath, f.path)} (${f.reasons.join(', ')})`);
|
|
437
|
+
}
|
|
438
|
+
return fileScores.slice(0, maxFiles).map(f => f.path);
|
|
439
|
+
}
|
|
440
|
+
/**
|
|
441
|
+
* Get import counts for files
|
|
442
|
+
*/
|
|
443
|
+
async getImportCounts(files) {
|
|
444
|
+
const importCounts = new Map();
|
|
445
|
+
// Quick scan for imports (simplified)
|
|
446
|
+
const importPatterns = [
|
|
447
|
+
/from\s+['"]([^'"]+)['"]/g,
|
|
448
|
+
/import\s+['"]([^'"]+)['"]/g,
|
|
449
|
+
/require\(['"]([^'"]+)['"]\)/g
|
|
450
|
+
];
|
|
451
|
+
// Normalize import path to file path
|
|
452
|
+
const normalizeImport = (importPath, fromFile) => {
|
|
453
|
+
// Skip external packages
|
|
454
|
+
if (!importPath.startsWith('.') && !importPath.startsWith('/')) {
|
|
455
|
+
return null;
|
|
456
|
+
}
|
|
457
|
+
const fromDir = path.dirname(fromFile);
|
|
458
|
+
let resolved = path.resolve(fromDir, importPath);
|
|
459
|
+
// Try common extensions
|
|
460
|
+
const extensions = ['.ts', '.tsx', '.js', '.jsx', '.json', ''];
|
|
461
|
+
for (const ext of extensions) {
|
|
462
|
+
const withExt = resolved + ext;
|
|
463
|
+
if (existsSync(withExt)) {
|
|
464
|
+
return withExt;
|
|
465
|
+
}
|
|
466
|
+
// Try index files
|
|
467
|
+
const indexPath = path.join(resolved, 'index' + ext);
|
|
468
|
+
if (existsSync(indexPath)) {
|
|
469
|
+
return indexPath;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
return null;
|
|
473
|
+
};
|
|
474
|
+
// Sample files for import counting (don't scan all)
|
|
475
|
+
const sampleSize = Math.min(1000, files.length);
|
|
476
|
+
const sample = files.slice(0, sampleSize);
|
|
477
|
+
for (const file of sample) {
|
|
478
|
+
try {
|
|
479
|
+
const content = await readFile(file.path, 'utf-8');
|
|
480
|
+
for (const pattern of importPatterns) {
|
|
481
|
+
let match;
|
|
482
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
483
|
+
const importPath = match[1];
|
|
484
|
+
const resolvedPath = normalizeImport(importPath, file.path);
|
|
485
|
+
if (resolvedPath) {
|
|
486
|
+
importCounts.set(resolvedPath, (importCounts.get(resolvedPath) || 0) + 1);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
catch {
|
|
492
|
+
// Skip files we can't read
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
return importCounts;
|
|
496
|
+
}
|
|
497
|
+
/**
|
|
498
|
+
* Get commit counts from git
|
|
499
|
+
*/
|
|
500
|
+
async getCommitCounts() {
|
|
501
|
+
const commitCounts = new Map();
|
|
502
|
+
if (!this.gitAvailable) {
|
|
503
|
+
return commitCounts;
|
|
504
|
+
}
|
|
505
|
+
try {
|
|
506
|
+
const { stdout } = await execAsync('git log --name-only --pretty=format: --since="1 year ago" | sort | uniq -c | sort -rn | head -2000', { cwd: this.projectPath, maxBuffer: 10 * 1024 * 1024 });
|
|
507
|
+
for (const line of stdout.split('\n')) {
|
|
508
|
+
const match = line.trim().match(/^\s*(\d+)\s+(.+)$/);
|
|
509
|
+
if (match) {
|
|
510
|
+
const count = parseInt(match[1], 10);
|
|
511
|
+
const filePath = path.join(this.projectPath, match[2]);
|
|
512
|
+
commitCounts.set(filePath, count);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
catch {
|
|
517
|
+
// Ignore errors
|
|
518
|
+
}
|
|
519
|
+
return commitCounts;
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* Get tier configuration
|
|
523
|
+
*/
|
|
524
|
+
getTierConfig() {
|
|
525
|
+
return TIER_CONFIG[this.options.tier];
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
export { TIER_CONFIG };
|
|
529
|
+
//# sourceMappingURL=enterprise-indexer.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "archicore",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.7",
|
|
4
4
|
"description": "AI Software Architect - code analysis, impact prediction, semantic search",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -80,6 +80,7 @@
|
|
|
80
80
|
"passport-github2": "^0.1.12",
|
|
81
81
|
"pg": "^8.13.0",
|
|
82
82
|
"ora": "^8.1.1",
|
|
83
|
+
"tar": "^7.4.3",
|
|
83
84
|
"tree-sitter": "^0.21.1",
|
|
84
85
|
"tree-sitter-javascript": "^0.21.4",
|
|
85
86
|
"tree-sitter-python": "^0.21.0",
|