archicore 0.4.2 → 0.4.4

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.
@@ -0,0 +1,708 @@
1
+ /**
2
+ * ArchiCore Context Builder
3
+ *
4
+ * Автоматически собирает релевантный контекст из проекта
5
+ * для умных ответов ИИ в диалоге
6
+ */
7
+ import { Logger } from '../utils/logger.js';
8
+ // Паттерны для определения намерения
9
+ const INTENT_PATTERNS = [
10
+ // Поиск символа
11
+ { pattern: /где\s+(функци[яю]|метод|класс|переменн[ая]|интерфейс)\s+(\w+)/i, intent: 'find_symbol' },
12
+ { pattern: /найди?\s+(функци[яю]|метод|класс)\s+(\w+)/i, intent: 'find_symbol' },
13
+ { pattern: /where\s+is\s+(function|method|class|variable)\s+(\w+)/i, intent: 'find_symbol' },
14
+ { pattern: /find\s+(function|method|class)\s+(\w+)/i, intent: 'find_symbol' },
15
+ { pattern: /покажи\s+(функци[яю]|метод|класс)\s+(\w+)/i, intent: 'find_symbol' },
16
+ // Поиск использования
17
+ { pattern: /где\s+используется\s+(\w+)/i, intent: 'find_usage' },
18
+ { pattern: /кто\s+вызывает\s+(\w+)/i, intent: 'find_usage' },
19
+ { pattern: /where\s+is\s+(\w+)\s+used/i, intent: 'find_usage' },
20
+ { pattern: /who\s+calls\s+(\w+)/i, intent: 'find_usage' },
21
+ // Объяснение потока
22
+ { pattern: /как\s+работает\s+(\w+)/i, intent: 'explain_flow' },
23
+ { pattern: /объясни\s+(\w+)/i, intent: 'explain_flow' },
24
+ { pattern: /how\s+does\s+(\w+)\s+work/i, intent: 'explain_flow' },
25
+ { pattern: /explain\s+(\w+)/i, intent: 'explain_flow' },
26
+ // Поиск багов
27
+ { pattern: /найди\s+(баги?|ошибки?|проблемы?)/i, intent: 'find_bugs' },
28
+ { pattern: /есть\s+ли\s+(баги?|ошибки?|проблемы?)/i, intent: 'find_bugs' },
29
+ { pattern: /find\s+(bugs?|errors?|issues?|problems?)/i, intent: 'find_bugs' },
30
+ { pattern: /check\s+for\s+(bugs?|errors?)/i, intent: 'find_bugs' },
31
+ { pattern: /что\s+не\s+так/i, intent: 'find_bugs' },
32
+ // Архитектура
33
+ { pattern: /архитектур[аы]/i, intent: 'architecture' },
34
+ { pattern: /структур[аы]\s+проекта/i, intent: 'architecture' },
35
+ { pattern: /architecture/i, intent: 'architecture' },
36
+ { pattern: /project\s+structure/i, intent: 'architecture' },
37
+ // Поиск файла
38
+ { pattern: /где\s+файл\s+(\w+)/i, intent: 'find_file' },
39
+ { pattern: /найди\s+файл\s+(\w+)/i, intent: 'find_file' },
40
+ { pattern: /where\s+is\s+file\s+(\w+)/i, intent: 'find_file' },
41
+ // Рефакторинг
42
+ { pattern: /как\s+улучшить/i, intent: 'refactor' },
43
+ { pattern: /как\s+рефакторить/i, intent: 'refactor' },
44
+ { pattern: /how\s+to\s+(improve|refactor)/i, intent: 'refactor' },
45
+ ];
46
+ // Паттерны багов
47
+ const BUG_PATTERNS = [
48
+ {
49
+ name: 'console-log-in-production',
50
+ pattern: /console\.(log|debug|info)\s*\(/,
51
+ type: 'smell',
52
+ severity: 'low',
53
+ message: 'Console.log в production коде',
54
+ suggestion: 'Используй Logger или удали debug вывод',
55
+ },
56
+ {
57
+ name: 'todo-fixme',
58
+ pattern: /(TODO|FIXME|HACK|XXX):/i,
59
+ type: 'warning',
60
+ severity: 'low',
61
+ message: 'Незавершённая задача в коде',
62
+ suggestion: 'Завершить или создать issue',
63
+ },
64
+ {
65
+ name: 'empty-catch',
66
+ pattern: /catch\s*\([^)]*\)\s*\{\s*\}/,
67
+ type: 'bug',
68
+ severity: 'medium',
69
+ message: 'Пустой catch блок - ошибки игнорируются',
70
+ suggestion: 'Добавь обработку или логирование ошибки',
71
+ },
72
+ {
73
+ name: 'hardcoded-password',
74
+ pattern: /(password|secret|api.?key)\s*[:=]\s*['"][^'"]+['"]/i,
75
+ type: 'security',
76
+ severity: 'critical',
77
+ message: 'Захардкоженный пароль/ключ',
78
+ suggestion: 'Используй переменные окружения',
79
+ },
80
+ {
81
+ name: 'sql-injection',
82
+ pattern: /(query|execute)\s*\(\s*[`"'].*\$\{/,
83
+ type: 'security',
84
+ severity: 'critical',
85
+ message: 'Возможная SQL инъекция',
86
+ suggestion: 'Используй параметризованные запросы',
87
+ },
88
+ {
89
+ name: 'eval-usage',
90
+ pattern: /\beval\s*\(/,
91
+ type: 'security',
92
+ severity: 'high',
93
+ message: 'Использование eval() - риск инъекции',
94
+ suggestion: 'Избегай eval, используй безопасные альтернативы',
95
+ },
96
+ {
97
+ name: 'any-type',
98
+ pattern: /:\s*any\b/,
99
+ type: 'smell',
100
+ severity: 'low',
101
+ message: 'Использование типа any',
102
+ suggestion: 'Добавь конкретный тип',
103
+ },
104
+ {
105
+ name: 'magic-number',
106
+ pattern: /[=<>]\s*\d{3,}\b/,
107
+ type: 'smell',
108
+ severity: 'low',
109
+ message: 'Magic number в коде',
110
+ suggestion: 'Вынеси в именованную константу',
111
+ },
112
+ {
113
+ name: 'no-await',
114
+ pattern: /async\s+\w+[^}]+\breturn\s+\w+\s*\.\s*\w+\s*\([^)]*\)\s*;?\s*\}/,
115
+ type: 'bug',
116
+ severity: 'medium',
117
+ message: 'Возможно отсутствует await',
118
+ suggestion: 'Проверь, нужен ли await для async функции',
119
+ },
120
+ ];
121
+ export class ContextBuilder {
122
+ symbols;
123
+ graph;
124
+ fileContents;
125
+ projectStats;
126
+ constructor(symbols, graph, fileContents, projectStats) {
127
+ this.symbols = symbols;
128
+ this.graph = graph;
129
+ this.fileContents = fileContents;
130
+ this.projectStats = {
131
+ totalFiles: fileContents.size,
132
+ totalSymbols: symbols.size,
133
+ languages: this.detectLanguages(),
134
+ framework: projectStats?.framework,
135
+ };
136
+ }
137
+ /**
138
+ * Главный метод - строит контекст для вопроса пользователя
139
+ */
140
+ async buildContext(userQuery) {
141
+ Logger.debug(`Building context for: "${userQuery}"`);
142
+ const intent = this.detectIntent(userQuery);
143
+ const entities = this.extractEntities(userQuery);
144
+ const context = {
145
+ intent,
146
+ query: userQuery,
147
+ symbols: [],
148
+ relevantFiles: [],
149
+ dependencies: [],
150
+ issues: [],
151
+ projectStats: this.projectStats,
152
+ };
153
+ // В зависимости от намерения собираем нужный контекст
154
+ switch (intent) {
155
+ case 'find_symbol':
156
+ case 'find_usage':
157
+ context.symbols = this.searchSymbols(entities);
158
+ context.dependencies = this.getSymbolDependencies(context.symbols);
159
+ break;
160
+ case 'explain_flow':
161
+ context.symbols = this.searchSymbols(entities);
162
+ context.dependencies = this.getCallChain(entities);
163
+ context.relevantFiles = this.getRelevantFiles(entities);
164
+ break;
165
+ case 'find_bugs':
166
+ context.issues = this.findBugs(entities);
167
+ context.relevantFiles = this.getFilesWithIssues(context.issues);
168
+ break;
169
+ case 'architecture':
170
+ context.relevantFiles = this.getArchitectureFiles();
171
+ context.dependencies = this.getMainDependencies();
172
+ break;
173
+ case 'find_file':
174
+ context.relevantFiles = this.searchFiles(entities);
175
+ break;
176
+ case 'refactor':
177
+ context.symbols = this.searchSymbols(entities);
178
+ context.issues = this.findIssuesInSymbols(context.symbols);
179
+ break;
180
+ default:
181
+ // Общий вопрос - ищем по ключевым словам
182
+ context.symbols = this.searchSymbols(entities);
183
+ context.relevantFiles = this.getRelevantFiles(entities);
184
+ }
185
+ Logger.debug(`Context built: ${context.symbols.length} symbols, ${context.relevantFiles.length} files, ${context.issues.length} issues`);
186
+ return context;
187
+ }
188
+ /**
189
+ * Определение намерения пользователя
190
+ */
191
+ detectIntent(query) {
192
+ for (const { pattern, intent } of INTENT_PATTERNS) {
193
+ if (pattern.test(query)) {
194
+ return intent;
195
+ }
196
+ }
197
+ return 'general';
198
+ }
199
+ /**
200
+ * Извлечение сущностей из запроса (имена функций, файлов и т.д.)
201
+ */
202
+ extractEntities(query) {
203
+ const entities = [];
204
+ // Извлекаем слова в кавычках
205
+ const quoted = query.match(/['"`]([^'"`]+)['"`]/g);
206
+ if (quoted) {
207
+ entities.push(...quoted.map(q => q.slice(1, -1)));
208
+ }
209
+ // Извлекаем camelCase и PascalCase слова
210
+ const camelCase = query.match(/\b[a-z]+(?:[A-Z][a-z]+)+\b/g);
211
+ if (camelCase) {
212
+ entities.push(...camelCase);
213
+ }
214
+ const pascalCase = query.match(/\b[A-Z][a-z]+(?:[A-Z][a-z]+)+\b/g);
215
+ if (pascalCase) {
216
+ entities.push(...pascalCase);
217
+ }
218
+ // Извлекаем слова после ключевых слов
219
+ const afterKeywords = query.match(/(?:функци[яю]|метод|класс|файл|function|method|class|file)\s+(\w+)/gi);
220
+ if (afterKeywords) {
221
+ for (const match of afterKeywords) {
222
+ const word = match.split(/\s+/).pop();
223
+ if (word)
224
+ entities.push(word);
225
+ }
226
+ }
227
+ // Извлекаем пути к файлам
228
+ const filePaths = query.match(/[\w\-./\\]+\.(ts|js|php|py|vue|jsx|tsx)/gi);
229
+ if (filePaths) {
230
+ entities.push(...filePaths);
231
+ }
232
+ return [...new Set(entities)];
233
+ }
234
+ /**
235
+ * Поиск символов по названию (fuzzy)
236
+ */
237
+ searchSymbols(entities) {
238
+ const results = [];
239
+ const searchTerms = entities.map(e => e.toLowerCase());
240
+ if (searchTerms.length === 0) {
241
+ return results;
242
+ }
243
+ for (const [name, symbol] of this.symbols) {
244
+ const nameLower = name.toLowerCase();
245
+ for (const term of searchTerms) {
246
+ const confidence = this.calculateMatchConfidence(nameLower, term);
247
+ if (confidence > 0.3) {
248
+ const fileContent = this.fileContents.get(symbol.filePath) || '';
249
+ const lines = fileContent.split('\n');
250
+ const snippet = this.getSnippet(lines, symbol.location.startLine, 3);
251
+ results.push({
252
+ name,
253
+ type: symbol.kind,
254
+ file: symbol.filePath,
255
+ line: symbol.location.startLine,
256
+ snippet,
257
+ usedIn: this.findUsages(name),
258
+ calls: this.findCalls(name),
259
+ calledBy: this.findCalledBy(name),
260
+ confidence,
261
+ });
262
+ break;
263
+ }
264
+ }
265
+ }
266
+ // Сортируем по релевантности
267
+ results.sort((a, b) => b.confidence - a.confidence);
268
+ return results.slice(0, 10);
269
+ }
270
+ /**
271
+ * Расчёт схожести строк (fuzzy matching)
272
+ */
273
+ calculateMatchConfidence(str, query) {
274
+ // Точное совпадение
275
+ if (str === query)
276
+ return 1;
277
+ // Содержит полностью
278
+ if (str.includes(query))
279
+ return 0.9;
280
+ // Начинается с
281
+ if (str.startsWith(query))
282
+ return 0.85;
283
+ // Заканчивается на
284
+ if (str.endsWith(query))
285
+ return 0.8;
286
+ // Частичное совпадение (для camelCase)
287
+ const strParts = str.split(/(?=[A-Z])|_|-/).map(p => p.toLowerCase());
288
+ const queryParts = query.split(/(?=[A-Z])|_|-/).map(p => p.toLowerCase());
289
+ let matches = 0;
290
+ for (const qPart of queryParts) {
291
+ if (strParts.some(sPart => sPart.includes(qPart))) {
292
+ matches++;
293
+ }
294
+ }
295
+ if (matches > 0) {
296
+ return 0.5 + (matches / queryParts.length) * 0.3;
297
+ }
298
+ return 0;
299
+ }
300
+ /**
301
+ * Получение сниппета кода вокруг строки
302
+ */
303
+ getSnippet(lines, line, context) {
304
+ const start = Math.max(0, line - context - 1);
305
+ const end = Math.min(lines.length, line + context);
306
+ return lines
307
+ .slice(start, end)
308
+ .map((l, i) => {
309
+ const lineNum = start + i + 1;
310
+ const marker = lineNum === line ? '→' : ' ';
311
+ return `${marker}${lineNum.toString().padStart(4)}│ ${l}`;
312
+ })
313
+ .join('\n');
314
+ }
315
+ /**
316
+ * Поиск мест использования символа
317
+ */
318
+ findUsages(symbolName) {
319
+ const usages = [];
320
+ const regex = new RegExp(`\\b${this.escapeRegex(symbolName)}\\b`, 'g');
321
+ for (const [file, content] of this.fileContents) {
322
+ const lines = content.split('\n');
323
+ for (let i = 0; i < lines.length; i++) {
324
+ if (regex.test(lines[i])) {
325
+ // Пропускаем определение самого символа
326
+ const symbol = this.symbols.get(symbolName);
327
+ if (symbol && symbol.filePath === file && symbol.location.startLine === i + 1) {
328
+ continue;
329
+ }
330
+ usages.push({
331
+ file,
332
+ line: i + 1,
333
+ snippet: lines[i].trim().substring(0, 100),
334
+ });
335
+ }
336
+ }
337
+ }
338
+ return usages.slice(0, 10);
339
+ }
340
+ /**
341
+ * Что вызывает данный символ
342
+ */
343
+ findCalls(symbolName) {
344
+ const symbol = this.symbols.get(symbolName);
345
+ if (!symbol)
346
+ return [];
347
+ const calls = [];
348
+ const node = this.graph.nodes.get(symbol.filePath);
349
+ if (node) {
350
+ const edges = this.graph.edges.get(symbol.filePath) || [];
351
+ for (const edge of edges) {
352
+ if (typeof edge === 'object' && 'target' in edge) {
353
+ calls.push(edge.target);
354
+ }
355
+ else if (typeof edge === 'string') {
356
+ calls.push(edge);
357
+ }
358
+ }
359
+ }
360
+ return calls.slice(0, 10);
361
+ }
362
+ /**
363
+ * Кто вызывает данный символ
364
+ */
365
+ findCalledBy(symbolName) {
366
+ const calledBy = [];
367
+ const symbol = this.symbols.get(symbolName);
368
+ if (!symbol)
369
+ return [];
370
+ for (const [file, edges] of this.graph.edges) {
371
+ for (const edge of edges) {
372
+ const target = typeof edge === 'object' && 'to' in edge
373
+ ? edge.to
374
+ : String(edge);
375
+ if (target === symbol.filePath || target.includes(symbolName)) {
376
+ calledBy.push(file);
377
+ break;
378
+ }
379
+ }
380
+ }
381
+ return calledBy.slice(0, 10);
382
+ }
383
+ /**
384
+ * Получение зависимостей для символов
385
+ */
386
+ getSymbolDependencies(symbols) {
387
+ const deps = [];
388
+ const files = new Set(symbols.map(s => s.file));
389
+ for (const file of files) {
390
+ const edges = this.graph.edges.get(file) || [];
391
+ for (const edge of edges) {
392
+ const target = typeof edge === 'object' && 'target' in edge
393
+ ? edge.target
394
+ : String(edge);
395
+ deps.push({
396
+ from: file,
397
+ to: target,
398
+ type: 'imports',
399
+ });
400
+ }
401
+ }
402
+ return deps.slice(0, 20);
403
+ }
404
+ /**
405
+ * Получение цепочки вызовов
406
+ */
407
+ getCallChain(entities) {
408
+ const chain = [];
409
+ const visited = new Set();
410
+ const traverse = (file, depth) => {
411
+ if (depth > 3 || visited.has(file))
412
+ return;
413
+ visited.add(file);
414
+ const edges = this.graph.edges.get(file) || [];
415
+ for (const edge of edges) {
416
+ const target = typeof edge === 'object' && 'target' in edge
417
+ ? edge.target
418
+ : String(edge);
419
+ chain.push({
420
+ from: file,
421
+ to: target,
422
+ type: 'calls',
423
+ });
424
+ traverse(target, depth + 1);
425
+ }
426
+ };
427
+ for (const entity of entities) {
428
+ const symbol = this.symbols.get(entity);
429
+ if (symbol) {
430
+ traverse(symbol.filePath, 0);
431
+ }
432
+ }
433
+ return chain;
434
+ }
435
+ /**
436
+ * Поиск релевантных файлов
437
+ */
438
+ getRelevantFiles(entities) {
439
+ const files = [];
440
+ const searchTerms = entities.map(e => e.toLowerCase());
441
+ for (const [path, content] of this.fileContents) {
442
+ let relevance = 0;
443
+ for (const term of searchTerms) {
444
+ if (path.toLowerCase().includes(term)) {
445
+ relevance += 0.5;
446
+ }
447
+ if (content.toLowerCase().includes(term)) {
448
+ relevance += 0.3;
449
+ }
450
+ }
451
+ if (relevance > 0) {
452
+ files.push({
453
+ path,
454
+ relevance,
455
+ snippet: content.substring(0, 200) + '...',
456
+ });
457
+ }
458
+ }
459
+ files.sort((a, b) => b.relevance - a.relevance);
460
+ return files.slice(0, 10);
461
+ }
462
+ /**
463
+ * Поиск багов в коде
464
+ */
465
+ findBugs(entities) {
466
+ const issues = [];
467
+ const filesToCheck = entities.length > 0
468
+ ? this.getRelevantFiles(entities).map(f => f.path)
469
+ : Array.from(this.fileContents.keys());
470
+ for (const file of filesToCheck.slice(0, 50)) {
471
+ const content = this.fileContents.get(file);
472
+ if (!content)
473
+ continue;
474
+ const lines = content.split('\n');
475
+ for (let i = 0; i < lines.length; i++) {
476
+ const line = lines[i];
477
+ for (const bugPattern of BUG_PATTERNS) {
478
+ if (bugPattern.pattern.test(line)) {
479
+ issues.push({
480
+ type: bugPattern.type,
481
+ severity: bugPattern.severity,
482
+ file,
483
+ line: i + 1,
484
+ message: bugPattern.message,
485
+ snippet: line.trim().substring(0, 100),
486
+ suggestion: bugPattern.suggestion,
487
+ });
488
+ }
489
+ }
490
+ }
491
+ }
492
+ // Сортируем по severity
493
+ const severityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
494
+ issues.sort((a, b) => severityOrder[a.severity] - severityOrder[b.severity]);
495
+ return issues.slice(0, 20);
496
+ }
497
+ /**
498
+ * Получение файлов с проблемами
499
+ */
500
+ getFilesWithIssues(issues) {
501
+ const fileMap = new Map();
502
+ for (const issue of issues) {
503
+ const count = fileMap.get(issue.file) || 0;
504
+ fileMap.set(issue.file, count + 1);
505
+ }
506
+ return Array.from(fileMap.entries())
507
+ .map(([path, count]) => ({
508
+ path,
509
+ relevance: count,
510
+ }))
511
+ .sort((a, b) => b.relevance - a.relevance);
512
+ }
513
+ /**
514
+ * Получение файлов архитектуры
515
+ */
516
+ getArchitectureFiles() {
517
+ const architecturePatterns = [
518
+ /index\.(ts|js|php)$/,
519
+ /main\.(ts|js|php)$/,
520
+ /app\.(ts|js|vue|php)$/,
521
+ /router/i,
522
+ /routes/i,
523
+ /controller/i,
524
+ /service/i,
525
+ /model/i,
526
+ /config/i,
527
+ ];
528
+ const files = [];
529
+ for (const path of this.fileContents.keys()) {
530
+ for (const pattern of architecturePatterns) {
531
+ if (pattern.test(path)) {
532
+ files.push({
533
+ path,
534
+ relevance: 1,
535
+ });
536
+ break;
537
+ }
538
+ }
539
+ }
540
+ return files.slice(0, 15);
541
+ }
542
+ /**
543
+ * Получение основных зависимостей проекта
544
+ */
545
+ getMainDependencies() {
546
+ const deps = [];
547
+ const entryPoints = ['index', 'main', 'app', 'server'];
548
+ for (const [file, edges] of this.graph.edges) {
549
+ const isEntryPoint = entryPoints.some(ep => file.toLowerCase().includes(ep));
550
+ if (isEntryPoint || edges.length > 5) {
551
+ for (const edge of edges.slice(0, 5)) {
552
+ const target = typeof edge === 'object' && 'target' in edge
553
+ ? edge.target
554
+ : String(edge);
555
+ deps.push({
556
+ from: file,
557
+ to: target,
558
+ type: 'imports',
559
+ });
560
+ }
561
+ }
562
+ }
563
+ return deps.slice(0, 30);
564
+ }
565
+ /**
566
+ * Поиск файлов по имени
567
+ */
568
+ searchFiles(entities) {
569
+ const files = [];
570
+ for (const path of this.fileContents.keys()) {
571
+ for (const entity of entities) {
572
+ if (path.toLowerCase().includes(entity.toLowerCase())) {
573
+ files.push({
574
+ path,
575
+ relevance: path.toLowerCase() === entity.toLowerCase() ? 1 : 0.5,
576
+ });
577
+ break;
578
+ }
579
+ }
580
+ }
581
+ files.sort((a, b) => b.relevance - a.relevance);
582
+ return files.slice(0, 10);
583
+ }
584
+ /**
585
+ * Поиск проблем в найденных символах
586
+ */
587
+ findIssuesInSymbols(symbols) {
588
+ const issues = [];
589
+ for (const symbol of symbols) {
590
+ const content = this.fileContents.get(symbol.file);
591
+ if (!content)
592
+ continue;
593
+ const lines = content.split('\n');
594
+ const startLine = Math.max(0, symbol.line - 1);
595
+ const endLine = Math.min(lines.length, symbol.line + 50);
596
+ for (let i = startLine; i < endLine; i++) {
597
+ const line = lines[i];
598
+ for (const bugPattern of BUG_PATTERNS) {
599
+ if (bugPattern.pattern.test(line)) {
600
+ issues.push({
601
+ type: bugPattern.type,
602
+ severity: bugPattern.severity,
603
+ file: symbol.file,
604
+ line: i + 1,
605
+ message: bugPattern.message,
606
+ snippet: line.trim().substring(0, 100),
607
+ suggestion: bugPattern.suggestion,
608
+ });
609
+ }
610
+ }
611
+ }
612
+ }
613
+ return issues;
614
+ }
615
+ /**
616
+ * Определение языков в проекте
617
+ */
618
+ detectLanguages() {
619
+ const extensions = new Map();
620
+ for (const path of this.fileContents.keys()) {
621
+ const ext = path.split('.').pop()?.toLowerCase() || '';
622
+ extensions.set(ext, (extensions.get(ext) || 0) + 1);
623
+ }
624
+ const langMap = {
625
+ ts: 'TypeScript',
626
+ tsx: 'TypeScript/React',
627
+ js: 'JavaScript',
628
+ jsx: 'JavaScript/React',
629
+ php: 'PHP',
630
+ py: 'Python',
631
+ vue: 'Vue.js',
632
+ java: 'Java',
633
+ go: 'Go',
634
+ rs: 'Rust',
635
+ rb: 'Ruby',
636
+ cs: 'C#',
637
+ };
638
+ return Array.from(extensions.entries())
639
+ .sort((a, b) => b[1] - a[1])
640
+ .slice(0, 5)
641
+ .map(([ext]) => langMap[ext] || ext)
642
+ .filter(Boolean);
643
+ }
644
+ /**
645
+ * Escape regex special characters
646
+ */
647
+ escapeRegex(str) {
648
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
649
+ }
650
+ /**
651
+ * Форматирование контекста для LLM промпта
652
+ */
653
+ formatForLLM(context) {
654
+ const parts = [];
655
+ parts.push(`## Контекст проекта`);
656
+ parts.push(`Языки: ${context.projectStats.languages.join(', ')}`);
657
+ parts.push(`Файлов: ${context.projectStats.totalFiles}, Символов: ${context.projectStats.totalSymbols}`);
658
+ parts.push('');
659
+ if (context.symbols.length > 0) {
660
+ parts.push(`## Найденные символы (${context.symbols.length})`);
661
+ for (const sym of context.symbols.slice(0, 5)) {
662
+ parts.push(`### ${sym.type} ${sym.name}`);
663
+ parts.push(`Файл: ${sym.file}:${sym.line}`);
664
+ parts.push('```');
665
+ parts.push(sym.snippet);
666
+ parts.push('```');
667
+ if (sym.usedIn.length > 0) {
668
+ parts.push(`Используется в: ${sym.usedIn.map(u => `${u.file}:${u.line}`).join(', ')}`);
669
+ }
670
+ parts.push('');
671
+ }
672
+ }
673
+ if (context.issues.length > 0) {
674
+ parts.push(`## Найденные проблемы (${context.issues.length})`);
675
+ for (const issue of context.issues.slice(0, 10)) {
676
+ const icon = issue.severity === 'critical' ? '🔴' :
677
+ issue.severity === 'high' ? '🟠' :
678
+ issue.severity === 'medium' ? '🟡' : '🔵';
679
+ parts.push(`${icon} **${issue.message}** (${issue.severity})`);
680
+ parts.push(` ${issue.file}:${issue.line}`);
681
+ parts.push(` \`${issue.snippet}\``);
682
+ parts.push(` 💡 ${issue.suggestion}`);
683
+ parts.push('');
684
+ }
685
+ }
686
+ if (context.relevantFiles.length > 0) {
687
+ parts.push(`## Релевантные файлы`);
688
+ for (const file of context.relevantFiles.slice(0, 5)) {
689
+ parts.push(`- ${file.path}`);
690
+ }
691
+ parts.push('');
692
+ }
693
+ if (context.dependencies.length > 0) {
694
+ parts.push(`## Зависимости`);
695
+ for (const dep of context.dependencies.slice(0, 10)) {
696
+ parts.push(`- ${dep.from} → ${dep.to}`);
697
+ }
698
+ }
699
+ return parts.join('\n');
700
+ }
701
+ }
702
+ /**
703
+ * Создание билдера контекста из данных проекта
704
+ */
705
+ export function createContextBuilder(symbols, graph, fileContents, projectStats) {
706
+ return new ContextBuilder(symbols, graph, fileContents, projectStats);
707
+ }
708
+ //# sourceMappingURL=context-builder.js.map