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.
- package/dist/analyzers/ai-narrator.d.ts +107 -0
- package/dist/analyzers/ai-narrator.js +632 -0
- package/dist/analyzers/context-builder.d.ts +150 -0
- package/dist/analyzers/context-builder.js +708 -0
- package/dist/analyzers/response-enhancer.d.ts +85 -0
- package/dist/analyzers/response-enhancer.js +632 -0
- package/dist/cli/commands/interactive.js +151 -1
- package/dist/cli/ui/prompt.js +1 -0
- package/dist/cli/utils/upload-utils.js +44 -18
- package/dist/orchestrator/index.js +67 -1
- package/dist/server/routes/api.js +70 -0
- package/dist/server/services/project-service.d.ts +11 -0
- package/dist/server/services/project-service.js +97 -2
- package/dist/types/index.d.ts +44 -0
- package/package.json +1 -1
|
@@ -520,6 +520,11 @@ async function handleCommand(input) {
|
|
|
520
520
|
case 'documentation':
|
|
521
521
|
await handleDocsCommand(args);
|
|
522
522
|
break;
|
|
523
|
+
case 'narrative':
|
|
524
|
+
case 'story':
|
|
525
|
+
case 'explain':
|
|
526
|
+
await handleNarrativeCommand();
|
|
527
|
+
break;
|
|
523
528
|
case 'status':
|
|
524
529
|
await handleStatusCommand();
|
|
525
530
|
break;
|
|
@@ -965,7 +970,8 @@ async function handleQuery(query) {
|
|
|
965
970
|
content: m.content,
|
|
966
971
|
}));
|
|
967
972
|
}
|
|
968
|
-
|
|
973
|
+
// Используем enhanced endpoint для расширенных ответов
|
|
974
|
+
const response = await apiFetch(`${config.serverUrl}/api/projects/${state.projectId}/ask-enhanced`, {
|
|
969
975
|
method: 'POST',
|
|
970
976
|
body: JSON.stringify({
|
|
971
977
|
question: query,
|
|
@@ -986,6 +992,55 @@ async function handleQuery(query) {
|
|
|
986
992
|
for (const line of lines) {
|
|
987
993
|
console.log(' ' + line);
|
|
988
994
|
}
|
|
995
|
+
// Показываем диаграммы если есть
|
|
996
|
+
if (data.diagrams && data.diagrams.length > 0) {
|
|
997
|
+
for (const diagram of data.diagrams) {
|
|
998
|
+
console.log();
|
|
999
|
+
console.log(colors.secondary(` 📊 ${diagram.title}:`));
|
|
1000
|
+
console.log(diagram.content);
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
// Показываем подсказки по производительности если есть
|
|
1004
|
+
if (data.performanceHints && data.performanceHints.length > 0) {
|
|
1005
|
+
console.log();
|
|
1006
|
+
console.log(colors.warning(' ⚡ Подсказки по производительности:'));
|
|
1007
|
+
for (const hint of data.performanceHints) {
|
|
1008
|
+
const icon = hint.severity === 'critical' ? '🔴' :
|
|
1009
|
+
hint.severity === 'warning' ? '🟡' : '🔵';
|
|
1010
|
+
console.log();
|
|
1011
|
+
console.log(colors.highlight(` ${icon} ${hint.type}: ${hint.description}`));
|
|
1012
|
+
console.log(colors.muted(` 📍 ${hint.file}:${hint.line}`));
|
|
1013
|
+
console.log(colors.info(` 💡 ${hint.suggestion}`));
|
|
1014
|
+
console.log(colors.muted(` 📈 ${hint.estimatedImpact}`));
|
|
1015
|
+
if (hint.codeExample) {
|
|
1016
|
+
console.log(colors.dim(hint.codeExample));
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
// Показываем сгенерированные тесты если есть
|
|
1021
|
+
if (data.generatedTests && data.generatedTests.length > 0) {
|
|
1022
|
+
console.log();
|
|
1023
|
+
console.log(colors.success(' 🧪 Сгенерированные тесты:'));
|
|
1024
|
+
for (const test of data.generatedTests) {
|
|
1025
|
+
console.log();
|
|
1026
|
+
console.log(colors.muted(` // Тест для ${test.functionName} (${test.testFramework})`));
|
|
1027
|
+
console.log(colors.dim(` // Файл: ${test.filePath}`));
|
|
1028
|
+
console.log(colors.dim(' ```' + test.language));
|
|
1029
|
+
const testLines = test.testCode.split('\n');
|
|
1030
|
+
for (const tl of testLines) {
|
|
1031
|
+
console.log(' ' + tl);
|
|
1032
|
+
}
|
|
1033
|
+
console.log(colors.dim(' ```'));
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
// Показываем follow-up suggestions если есть
|
|
1037
|
+
if (data.followUpSuggestions && data.followUpSuggestions.length > 0) {
|
|
1038
|
+
console.log();
|
|
1039
|
+
console.log(colors.secondary(' 📌 Связанные вопросы:'));
|
|
1040
|
+
data.followUpSuggestions.forEach((suggestion, i) => {
|
|
1041
|
+
console.log(colors.muted(` [${i + 1}] ${suggestion.icon} ${suggestion.question}`));
|
|
1042
|
+
});
|
|
1043
|
+
}
|
|
989
1044
|
// Save assistant response to history
|
|
990
1045
|
if (state.conversationSession) {
|
|
991
1046
|
await addMessage(state.conversationSession, 'assistant', answer, {
|
|
@@ -1389,6 +1444,101 @@ async function handleDocsCommand(args) {
|
|
|
1389
1444
|
throw error;
|
|
1390
1445
|
}
|
|
1391
1446
|
}
|
|
1447
|
+
/**
|
|
1448
|
+
* /narrative command - AI Narrator: человекопонятное описание проекта
|
|
1449
|
+
*
|
|
1450
|
+
* Показывает:
|
|
1451
|
+
* - Что это за проект
|
|
1452
|
+
* - Стек технологий
|
|
1453
|
+
* - Архитектуру и паттерны
|
|
1454
|
+
* - Проблемные места
|
|
1455
|
+
* - Рекомендации
|
|
1456
|
+
*/
|
|
1457
|
+
async function handleNarrativeCommand() {
|
|
1458
|
+
if (!state.projectId) {
|
|
1459
|
+
printError('No project indexed');
|
|
1460
|
+
printInfo('Use /index first');
|
|
1461
|
+
return;
|
|
1462
|
+
}
|
|
1463
|
+
const spinner = createSpinner('Analyzing project architecture...').start();
|
|
1464
|
+
try {
|
|
1465
|
+
const config = await loadConfig();
|
|
1466
|
+
const response = await apiFetch(`${config.serverUrl}/api/projects/${state.projectId}/narrative?language=ru`);
|
|
1467
|
+
if (!response.ok) {
|
|
1468
|
+
const error = await response.json().catch(() => ({}));
|
|
1469
|
+
throw new Error(error.error || 'Failed to generate narrative');
|
|
1470
|
+
}
|
|
1471
|
+
const data = await response.json();
|
|
1472
|
+
spinner.succeed('Analysis complete');
|
|
1473
|
+
// Красивый вывод в консоль
|
|
1474
|
+
console.log();
|
|
1475
|
+
console.log(colors.primary('═══════════════════════════════════════════════════════════════'));
|
|
1476
|
+
console.log(colors.primary(' AI NARRATOR - Анализ проекта'));
|
|
1477
|
+
console.log(colors.primary('═══════════════════════════════════════════════════════════════'));
|
|
1478
|
+
console.log();
|
|
1479
|
+
const report = data.report;
|
|
1480
|
+
// Обзор
|
|
1481
|
+
console.log(colors.highlight('📋 ОБЗОР'));
|
|
1482
|
+
console.log(` Тип: ${colors.secondary(report.summary.projectType)}`);
|
|
1483
|
+
console.log(` Язык: ${colors.secondary(report.summary.primaryLanguage)}`);
|
|
1484
|
+
console.log(` Файлов: ${colors.secondary(String(report.summary.totalFiles))}`);
|
|
1485
|
+
console.log(` Символов: ${colors.secondary(String(report.summary.totalSymbols))}`);
|
|
1486
|
+
console.log(` Строк кода: ${colors.secondary(report.summary.linesOfCode.toLocaleString())}`);
|
|
1487
|
+
console.log();
|
|
1488
|
+
// Стек технологий
|
|
1489
|
+
console.log(colors.highlight('🛠️ СТЕК ТЕХНОЛОГИЙ'));
|
|
1490
|
+
if (report.techStack.frontend.length > 0) {
|
|
1491
|
+
console.log(` Frontend: ${colors.secondary(report.techStack.frontend.join(', '))}`);
|
|
1492
|
+
}
|
|
1493
|
+
if (report.techStack.backend.length > 0) {
|
|
1494
|
+
console.log(` Backend: ${colors.secondary(report.techStack.backend.join(', '))}`);
|
|
1495
|
+
}
|
|
1496
|
+
if (report.techStack.database.length > 0) {
|
|
1497
|
+
console.log(` Database: ${colors.secondary(report.techStack.database.join(', '))}`);
|
|
1498
|
+
}
|
|
1499
|
+
console.log();
|
|
1500
|
+
// Архитектура
|
|
1501
|
+
console.log(colors.highlight('🏗️ АРХИТЕКТУРА'));
|
|
1502
|
+
console.log(` Паттерн: ${colors.secondary(report.architecture.detected)} (${report.architecture.confidence}%)`);
|
|
1503
|
+
if (report.architecture.patterns.length > 0) {
|
|
1504
|
+
console.log(' Используются:');
|
|
1505
|
+
for (const pattern of report.architecture.patterns.slice(0, 5)) {
|
|
1506
|
+
console.log(` • ${pattern.name} (${pattern.confidence}%)`);
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
console.log();
|
|
1510
|
+
// Проблемы
|
|
1511
|
+
if (report.problems.length > 0) {
|
|
1512
|
+
console.log(colors.highlight('⚠️ ПРОБЛЕМНЫЕ МЕСТА'));
|
|
1513
|
+
for (const problem of report.problems.slice(0, 5)) {
|
|
1514
|
+
const icon = problem.severity === 'critical' ? '🔴' :
|
|
1515
|
+
problem.severity === 'warning' ? '🟡' : '🔵';
|
|
1516
|
+
console.log(` ${icon} ${colors.error(problem.title)}`);
|
|
1517
|
+
console.log(` ${problem.description}`);
|
|
1518
|
+
console.log(` 💡 ${colors.muted(problem.suggestion)}`);
|
|
1519
|
+
console.log();
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
// Рекомендации
|
|
1523
|
+
if (report.recommendations.length > 0) {
|
|
1524
|
+
console.log(colors.highlight('💡 РЕКОМЕНДАЦИИ'));
|
|
1525
|
+
for (let i = 0; i < Math.min(report.recommendations.length, 5); i++) {
|
|
1526
|
+
console.log(` ${i + 1}. ${report.recommendations[i]}`);
|
|
1527
|
+
}
|
|
1528
|
+
console.log();
|
|
1529
|
+
}
|
|
1530
|
+
console.log(colors.primary('═══════════════════════════════════════════════════════════════'));
|
|
1531
|
+
console.log();
|
|
1532
|
+
// Также выводим полный Markdown нарратив
|
|
1533
|
+
printInfo('Полный отчёт в формате Markdown:');
|
|
1534
|
+
console.log();
|
|
1535
|
+
console.log(colors.muted(data.narrative));
|
|
1536
|
+
}
|
|
1537
|
+
catch (error) {
|
|
1538
|
+
spinner.fail('Analysis failed');
|
|
1539
|
+
printError(error instanceof Error ? error.message : 'Unknown error');
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1392
1542
|
/**
|
|
1393
1543
|
* /history command - View and search conversation history
|
|
1394
1544
|
*
|
package/dist/cli/ui/prompt.js
CHANGED
|
@@ -70,6 +70,7 @@ export function printHelp() {
|
|
|
70
70
|
console.log();
|
|
71
71
|
const commands = [
|
|
72
72
|
['/index', 'Index current project'],
|
|
73
|
+
['/narrative', 'AI Narrator - describe architecture'],
|
|
73
74
|
['/analyze [desc]', 'Run impact analysis'],
|
|
74
75
|
['/search <query>', 'Semantic code search'],
|
|
75
76
|
['/dead-code', 'Find dead code'],
|
|
@@ -14,17 +14,21 @@ function debugLog(message) {
|
|
|
14
14
|
debugLog(` ${message}`);
|
|
15
15
|
}
|
|
16
16
|
}
|
|
17
|
-
// Лимиты для
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const
|
|
21
|
-
const
|
|
22
|
-
const
|
|
23
|
-
//
|
|
24
|
-
|
|
25
|
-
const
|
|
17
|
+
// Лимиты для single request upload (оптимизировано для средних проектов)
|
|
18
|
+
// ВАЖНО: chunked upload требует одного инстанса сервера (in-memory sessions)
|
|
19
|
+
// Для PM2 cluster mode используем single request до 50K символов
|
|
20
|
+
const MAX_PAYLOAD_SIZE = 50 * 1024 * 1024; // 50MB - увеличено для single request
|
|
21
|
+
const SINGLE_REQUEST_MAX_SYMBOLS = 50000; // До 50K символов - single request
|
|
22
|
+
const UPLOAD_TIMEOUT = 600000; // 10 минут для больших проектов
|
|
23
|
+
const MAX_RETRIES = 5; // Retry для нестабильных соединений
|
|
24
|
+
// Лимиты для chunked upload (только для очень больших проектов >50K символов)
|
|
25
|
+
const MAX_SYMBOLS_PER_CHUNK = 5000; // Больше символов на chunk
|
|
26
|
+
const MAX_FILES_PER_CHUNK = 100; // Больше файлов на chunk
|
|
27
|
+
// Лимиты для минимальной загрузки (fallback)
|
|
28
|
+
const MINIMAL_MAX_SYMBOLS = 20000;
|
|
29
|
+
const MINIMAL_MAX_FILES = 1000;
|
|
26
30
|
// Порог для "очень большого" проекта (пропускаем fileContents)
|
|
27
|
-
const VERY_LARGE_PROJECT_SYMBOLS =
|
|
31
|
+
const VERY_LARGE_PROJECT_SYMBOLS = 100000;
|
|
28
32
|
/**
|
|
29
33
|
* Определение размера JSON в байтах
|
|
30
34
|
*/
|
|
@@ -221,21 +225,43 @@ export async function uploadIndexData(projectId, data, onProgress) {
|
|
|
221
225
|
const url = `${config.serverUrl}/api/projects/${projectId}/upload-index`;
|
|
222
226
|
// Оценка размера данных
|
|
223
227
|
const estimatedSize = estimateJsonSize(data);
|
|
224
|
-
const isLargeProject = estimatedSize > MAX_PAYLOAD_SIZE ||
|
|
225
|
-
data.symbols.length > MAX_SYMBOLS_PER_CHUNK * 2;
|
|
226
228
|
onProgress?.({
|
|
227
229
|
phase: 'preparing',
|
|
228
230
|
current: 0,
|
|
229
231
|
total: 100,
|
|
230
|
-
message: `Preparing upload (${(estimatedSize / 1024 / 1024).toFixed(1)} MB)...`,
|
|
232
|
+
message: `Preparing upload (${(estimatedSize / 1024 / 1024).toFixed(1)} MB, ${data.symbols.length} symbols)...`,
|
|
231
233
|
});
|
|
232
|
-
//
|
|
233
|
-
|
|
234
|
+
// ВАЖНО: Chunked upload НЕ работает с PM2 cluster mode (разные процессы = разная память)
|
|
235
|
+
// Поэтому для проектов до 50K символов ВСЕГДА используем single request
|
|
236
|
+
// Если payload слишком большой - урезаем данные вместо chunked upload
|
|
237
|
+
if (data.symbols.length <= SINGLE_REQUEST_MAX_SYMBOLS) {
|
|
238
|
+
// Проект средний - используем single request
|
|
239
|
+
// Если payload > 50MB, урезаем fileContents
|
|
240
|
+
let uploadData = data;
|
|
241
|
+
if (estimatedSize > MAX_PAYLOAD_SIZE) {
|
|
242
|
+
debugLog(` Payload ${(estimatedSize / 1024 / 1024).toFixed(1)}MB exceeds limit, trimming fileContents`);
|
|
243
|
+
onProgress?.({
|
|
244
|
+
phase: 'preparing',
|
|
245
|
+
current: 0,
|
|
246
|
+
total: 100,
|
|
247
|
+
message: `Optimizing payload (trimming file contents)...`,
|
|
248
|
+
});
|
|
249
|
+
// Урезаем fileContents чтобы уместиться в лимит
|
|
250
|
+
const maxFileContents = Math.floor(data.fileContents.length * (MAX_PAYLOAD_SIZE / estimatedSize));
|
|
251
|
+
uploadData = {
|
|
252
|
+
...data,
|
|
253
|
+
fileContents: data.fileContents.slice(0, Math.max(maxFileContents, 100)),
|
|
254
|
+
};
|
|
255
|
+
const newSize = estimateJsonSize(uploadData);
|
|
256
|
+
debugLog(` Trimmed payload to ${(newSize / 1024 / 1024).toFixed(1)}MB (${uploadData.fileContents.length} files)`);
|
|
257
|
+
}
|
|
234
258
|
debugLog(` Using single request upload for ${data.symbols.length} symbols`);
|
|
235
|
-
return uploadSingleRequest(url, projectId,
|
|
259
|
+
return uploadSingleRequest(url, projectId, uploadData, config.accessToken || '', onProgress);
|
|
236
260
|
}
|
|
237
|
-
//
|
|
238
|
-
|
|
261
|
+
// Проект очень большой (>50K символов) - chunked upload
|
|
262
|
+
// ВНИМАНИЕ: требует PM2 -i 1 (один инстанс)
|
|
263
|
+
debugLog(` Using chunked upload for very large project (${data.symbols.length} symbols)`);
|
|
264
|
+
console.log(' ⚠ Very large project (>50K symbols). Chunked upload requires single server instance (PM2 -i 1).');
|
|
239
265
|
return uploadChunked(url, projectId, data, config.accessToken || '', onProgress);
|
|
240
266
|
}
|
|
241
267
|
/**
|
|
@@ -391,6 +391,72 @@ A: "✅ Компонент Comments найден в 3 файлах:
|
|
|
391
391
|
prompt += `- ${bc.name}: ${bc.description}\n`;
|
|
392
392
|
}
|
|
393
393
|
}
|
|
394
|
+
// === SMART CONTEXT: умный контекст от ContextBuilder ===
|
|
395
|
+
if (context?.smartContext) {
|
|
396
|
+
const sc = context.smartContext;
|
|
397
|
+
prompt += '\n\n###SMART CONTEXT (ТОЧНЫЙ ПОИСК)###\n';
|
|
398
|
+
prompt += `Намерение пользователя: ${sc.intent}\n`;
|
|
399
|
+
prompt += `Проект: ${sc.projectStats.totalFiles} файлов, ${sc.projectStats.totalSymbols} символов\n`;
|
|
400
|
+
prompt += `Языки: ${sc.projectStats.languages.join(', ')}\n`;
|
|
401
|
+
// Найденные символы с кодом
|
|
402
|
+
if (sc.symbols.length > 0) {
|
|
403
|
+
prompt += `\n## НАЙДЕННЫЕ СИМВОЛЫ (${sc.symbols.length}):\n`;
|
|
404
|
+
prompt += `⚠️ ЭТО ТОЧНЫЕ РЕЗУЛЬТАТЫ ПОИСКА - используй их в ответе!\n\n`;
|
|
405
|
+
for (const sym of sc.symbols.slice(0, 5)) {
|
|
406
|
+
const cleanPath = sanitizePath(sym.file);
|
|
407
|
+
prompt += `### ${sym.type.toUpperCase()}: ${sym.name}\n`;
|
|
408
|
+
prompt += `📍 Файл: ${cleanPath}:${sym.line}\n`;
|
|
409
|
+
prompt += `Уверенность: ${Math.round(sym.confidence * 100)}%\n`;
|
|
410
|
+
prompt += '```\n' + sym.snippet + '\n```\n';
|
|
411
|
+
if (sym.usedIn.length > 0) {
|
|
412
|
+
prompt += `📎 Используется в ${sym.usedIn.length} местах:\n`;
|
|
413
|
+
for (const usage of sym.usedIn.slice(0, 3)) {
|
|
414
|
+
const usageCleanPath = sanitizePath(usage.file);
|
|
415
|
+
prompt += ` - ${usageCleanPath}:${usage.line}: \`${usage.snippet}\`\n`;
|
|
416
|
+
}
|
|
417
|
+
if (sym.usedIn.length > 3) {
|
|
418
|
+
prompt += ` ... и ещё ${sym.usedIn.length - 3} мест(а)\n`;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
if (sym.calledBy.length > 0) {
|
|
422
|
+
prompt += `📞 Вызывается из: ${sym.calledBy.slice(0, 5).map(f => sanitizePath(f)).join(', ')}\n`;
|
|
423
|
+
}
|
|
424
|
+
prompt += '\n';
|
|
425
|
+
}
|
|
426
|
+
if (sc.symbols.length > 5) {
|
|
427
|
+
prompt += `... и ещё ${sc.symbols.length - 5} символов найдено\n`;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
// Найденные проблемы (баги, уязвимости)
|
|
431
|
+
if (sc.issues.length > 0) {
|
|
432
|
+
prompt += `\n## НАЙДЕННЫЕ ПРОБЛЕМЫ (${sc.issues.length}):\n`;
|
|
433
|
+
prompt += `⚠️ ОБЯЗАТЕЛЬНО УПОМЯНИ ЭТИ ПРОБЛЕМЫ В ОТВЕТЕ!\n\n`;
|
|
434
|
+
const severityIcons = {
|
|
435
|
+
critical: '🔴 CRITICAL',
|
|
436
|
+
high: '🟠 HIGH',
|
|
437
|
+
medium: '🟡 MEDIUM',
|
|
438
|
+
low: '🔵 LOW'
|
|
439
|
+
};
|
|
440
|
+
for (const issue of sc.issues.slice(0, 10)) {
|
|
441
|
+
const cleanPath = sanitizePath(issue.file);
|
|
442
|
+
prompt += `${severityIcons[issue.severity]}: ${issue.message}\n`;
|
|
443
|
+
prompt += ` 📍 ${cleanPath}:${issue.line}\n`;
|
|
444
|
+
prompt += ` Код: \`${issue.snippet}\`\n`;
|
|
445
|
+
prompt += ` 💡 ${issue.suggestion}\n\n`;
|
|
446
|
+
}
|
|
447
|
+
if (sc.issues.length > 10) {
|
|
448
|
+
prompt += `... и ещё ${sc.issues.length - 10} проблем\n`;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
// Зависимости
|
|
452
|
+
if (sc.dependencies.length > 0) {
|
|
453
|
+
prompt += `\n## ЗАВИСИМОСТИ:\n`;
|
|
454
|
+
for (const dep of sc.dependencies.slice(0, 10)) {
|
|
455
|
+
prompt += ` ${sanitizePath(dep.from)} → ${sanitizePath(dep.to)}\n`;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
prompt += '\n###END SMART CONTEXT###\n';
|
|
459
|
+
}
|
|
394
460
|
if (context?.semanticMemory && context.semanticMemory.length > 0) {
|
|
395
461
|
const totalResults = context.semanticMemory.length;
|
|
396
462
|
const maxResults = Math.min(totalResults, 30); // Увеличено с 10 до 30
|
|
@@ -419,7 +485,7 @@ A: "✅ Компонент Comments найден в 3 файлах:
|
|
|
419
485
|
}
|
|
420
486
|
prompt += '\n###END PROJECT FILES###';
|
|
421
487
|
}
|
|
422
|
-
else {
|
|
488
|
+
else if (!context?.smartContext) {
|
|
423
489
|
prompt += '\n\n###PROJECT FILES: EMPTY###\nNo code indexed. Respond: "Проект не проиндексирован. Запустите индексацию."';
|
|
424
490
|
}
|
|
425
491
|
return prompt;
|
|
@@ -528,6 +528,54 @@ apiRouter.post('/projects/:id/ask', authMiddleware, checkProjectAccess, async (r
|
|
|
528
528
|
res.status(500).json({ error: 'Failed to process question' });
|
|
529
529
|
}
|
|
530
530
|
});
|
|
531
|
+
/**
|
|
532
|
+
* POST /api/projects/:id/ask-enhanced
|
|
533
|
+
* Задать вопрос AI-архитектору с расширенным ответом
|
|
534
|
+
* Включает: follow-up suggestions, диаграммы, тесты, performance hints
|
|
535
|
+
*/
|
|
536
|
+
apiRouter.post('/projects/:id/ask-enhanced', authMiddleware, checkProjectAccess, async (req, res) => {
|
|
537
|
+
try {
|
|
538
|
+
const { id } = req.params;
|
|
539
|
+
const { question, language } = req.body;
|
|
540
|
+
const userId = getUserId(req);
|
|
541
|
+
if (!question) {
|
|
542
|
+
res.status(400).json({ error: 'Question is required' });
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
// Input validation
|
|
546
|
+
if (typeof question !== 'string') {
|
|
547
|
+
res.status(400).json({ error: 'Invalid question format' });
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
const MAX_QUESTION_LENGTH = 5000;
|
|
551
|
+
if (question.length > MAX_QUESTION_LENGTH) {
|
|
552
|
+
res.status(400).json({ error: `Question too long. Maximum ${MAX_QUESTION_LENGTH} characters.` });
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
// Sanitize question
|
|
556
|
+
const sanitizedQuestion = question
|
|
557
|
+
.replace(/\0/g, '')
|
|
558
|
+
.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F]/g, '');
|
|
559
|
+
// Check request limit
|
|
560
|
+
if (userId) {
|
|
561
|
+
const usageResult = await authService.checkAndUpdateUsage(userId, 'request');
|
|
562
|
+
if (!usageResult.allowed) {
|
|
563
|
+
res.status(429).json({
|
|
564
|
+
error: 'Request limit reached',
|
|
565
|
+
message: `You have reached your daily request limit (${usageResult.limit})`,
|
|
566
|
+
usage: { used: usageResult.limit, limit: usageResult.limit, remaining: 0 }
|
|
567
|
+
});
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
const enhancedResponse = await projectService.askArchitectEnhanced(id, sanitizedQuestion, language || 'en');
|
|
572
|
+
res.json(enhancedResponse);
|
|
573
|
+
}
|
|
574
|
+
catch (error) {
|
|
575
|
+
Logger.error('Failed to ask architect (enhanced):', error);
|
|
576
|
+
res.status(500).json({ error: 'Failed to process question' });
|
|
577
|
+
}
|
|
578
|
+
});
|
|
531
579
|
/**
|
|
532
580
|
* GET /api/projects/:id/stats
|
|
533
581
|
* Получить статистику проекта
|
|
@@ -755,6 +803,28 @@ apiRouter.post('/projects/:id/documentation', authMiddleware, checkProjectAccess
|
|
|
755
803
|
res.status(500).json({ error: 'Failed to generate documentation' });
|
|
756
804
|
}
|
|
757
805
|
});
|
|
806
|
+
/**
|
|
807
|
+
* GET /api/projects/:id/narrative
|
|
808
|
+
* AI Narrator - человекопонятное описание архитектуры проекта
|
|
809
|
+
*/
|
|
810
|
+
apiRouter.get('/projects/:id/narrative', authMiddleware, checkProjectAccess, async (req, res) => {
|
|
811
|
+
try {
|
|
812
|
+
const { id } = req.params;
|
|
813
|
+
const language = req.query.language || 'ru';
|
|
814
|
+
const report = await projectService.getNarrative(id, language);
|
|
815
|
+
res.json({
|
|
816
|
+
success: true,
|
|
817
|
+
report,
|
|
818
|
+
// Отдельно текстовый нарратив для удобства
|
|
819
|
+
narrative: language === 'ru' ? report.narrativeRu : report.narrative,
|
|
820
|
+
});
|
|
821
|
+
}
|
|
822
|
+
catch (error) {
|
|
823
|
+
Logger.error('Failed to generate narrative:', error);
|
|
824
|
+
const errorMessage = error instanceof Error ? error.message : 'Failed to generate narrative';
|
|
825
|
+
res.status(500).json({ error: errorMessage });
|
|
826
|
+
}
|
|
827
|
+
});
|
|
758
828
|
/**
|
|
759
829
|
* GET /api/projects/:id/index-progress
|
|
760
830
|
* Server-Sent Events endpoint for real-time indexing progress
|
|
@@ -12,6 +12,8 @@ import { DuplicationResult } from '../../analyzers/duplication.js';
|
|
|
12
12
|
import { SecurityResult } from '../../analyzers/security.js';
|
|
13
13
|
import { RefactoringResult } from '../../refactoring/index.js';
|
|
14
14
|
import { ExportData } from '../../export/index.js';
|
|
15
|
+
import { NarratorReport } from '../../analyzers/ai-narrator.js';
|
|
16
|
+
import { EnhancedResponse } from '../../analyzers/response-enhancer.js';
|
|
15
17
|
export interface Project {
|
|
16
18
|
id: string;
|
|
17
19
|
name: string;
|
|
@@ -89,6 +91,11 @@ export declare class ProjectService {
|
|
|
89
91
|
}>;
|
|
90
92
|
semanticSearch(projectId: string, query: string, limit?: number): Promise<unknown[]>;
|
|
91
93
|
askArchitect(projectId: string, question: string, language?: 'en' | 'ru'): Promise<string>;
|
|
94
|
+
/**
|
|
95
|
+
* Расширенный ответ архитектора с дополнительными инструментами
|
|
96
|
+
* Включает: follow-up suggestions, диаграммы, тесты, performance hints
|
|
97
|
+
*/
|
|
98
|
+
askArchitectEnhanced(projectId: string, question: string, language?: 'en' | 'ru'): Promise<EnhancedResponse>;
|
|
92
99
|
getStatistics(projectId: string): Promise<{
|
|
93
100
|
project: Project;
|
|
94
101
|
indexStats: ProjectStats | null;
|
|
@@ -188,5 +195,9 @@ export declare class ProjectService {
|
|
|
188
195
|
* Построить текстовое представление структуры файлов
|
|
189
196
|
*/
|
|
190
197
|
private buildFileStructure;
|
|
198
|
+
/**
|
|
199
|
+
* AI Narrator - человекопонятное описание архитектуры проекта
|
|
200
|
+
*/
|
|
201
|
+
getNarrative(projectId: string, _language?: 'en' | 'ru'): Promise<NarratorReport>;
|
|
191
202
|
}
|
|
192
203
|
//# sourceMappingURL=project-service.d.ts.map
|
|
@@ -22,6 +22,9 @@ import { DuplicationDetector } from '../../analyzers/duplication.js';
|
|
|
22
22
|
import { SecurityAnalyzer } from '../../analyzers/security.js';
|
|
23
23
|
import { RefactoringEngine } from '../../refactoring/index.js';
|
|
24
24
|
import { GitHubService } from '../../github/github-service.js';
|
|
25
|
+
import { AINarrator } from '../../analyzers/ai-narrator.js';
|
|
26
|
+
import { ContextBuilder } from '../../analyzers/context-builder.js';
|
|
27
|
+
import { createResponseEnhancer } from '../../analyzers/response-enhancer.js';
|
|
25
28
|
/**
|
|
26
29
|
* Sanitize file paths to hide server directories
|
|
27
30
|
* Converts absolute paths to relative project paths
|
|
@@ -430,6 +433,20 @@ export class ProjectService {
|
|
|
430
433
|
const projectMetadata = analyzeProjectStack(project.path);
|
|
431
434
|
// Загружаем содержимое файлов для полного контекста
|
|
432
435
|
const fileContents = await this.getFileContents(projectId);
|
|
436
|
+
// === SMART CONTEXT: используем ContextBuilder для умного поиска ===
|
|
437
|
+
let smartContext = null;
|
|
438
|
+
if (data.symbols && data.graph) {
|
|
439
|
+
try {
|
|
440
|
+
const contextBuilder = new ContextBuilder(data.symbols, data.graph, fileContents, {
|
|
441
|
+
framework: projectMetadata?.framework,
|
|
442
|
+
});
|
|
443
|
+
smartContext = await contextBuilder.buildContext(question);
|
|
444
|
+
Logger.info(`Smart context: intent=${smartContext.intent}, symbols=${smartContext.symbols.length}, issues=${smartContext.issues.length}`);
|
|
445
|
+
}
|
|
446
|
+
catch (err) {
|
|
447
|
+
Logger.warn('Smart context builder failed:', err);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
433
450
|
// Ищем релевантный контекст (если есть семантическая память)
|
|
434
451
|
let searchResults = [];
|
|
435
452
|
if (data.semanticMemory) {
|
|
@@ -490,15 +507,72 @@ export class ProjectService {
|
|
|
490
507
|
}
|
|
491
508
|
}
|
|
492
509
|
}
|
|
493
|
-
Logger.debug(`Passing ${searchResults.length} context items + project metadata to AI`);
|
|
510
|
+
Logger.debug(`Passing ${searchResults.length} context items + smart context + project metadata to AI`);
|
|
494
511
|
const answer = await data.orchestrator.answerArchitecturalQuestion(question, {
|
|
495
512
|
architecture: data.architecture.getModel(),
|
|
496
513
|
semanticMemory: searchResults,
|
|
497
514
|
language,
|
|
498
|
-
projectMetadata: projectMetadata || undefined
|
|
515
|
+
projectMetadata: projectMetadata || undefined,
|
|
516
|
+
smartContext: smartContext || undefined
|
|
499
517
|
});
|
|
500
518
|
return answer;
|
|
501
519
|
}
|
|
520
|
+
/**
|
|
521
|
+
* Расширенный ответ архитектора с дополнительными инструментами
|
|
522
|
+
* Включает: follow-up suggestions, диаграммы, тесты, performance hints
|
|
523
|
+
*/
|
|
524
|
+
async askArchitectEnhanced(projectId, question, language = 'en') {
|
|
525
|
+
const project = this.projects.get(projectId);
|
|
526
|
+
if (!project) {
|
|
527
|
+
throw new Error(`Project not found: ${projectId}`);
|
|
528
|
+
}
|
|
529
|
+
const data = await this.getProjectData(projectId);
|
|
530
|
+
const fileContents = await this.getFileContents(projectId);
|
|
531
|
+
// Строим умный контекст
|
|
532
|
+
let smartContext = null;
|
|
533
|
+
if (data.symbols && data.graph) {
|
|
534
|
+
const contextBuilder = new ContextBuilder(data.symbols, data.graph, fileContents);
|
|
535
|
+
smartContext = await contextBuilder.buildContext(question);
|
|
536
|
+
}
|
|
537
|
+
// Получаем базовый ответ
|
|
538
|
+
const answer = await this.askArchitect(projectId, question, language);
|
|
539
|
+
// Создаём энхансер
|
|
540
|
+
if (!data.graph || !data.symbols) {
|
|
541
|
+
// Возвращаем минимальный ответ без улучшений
|
|
542
|
+
return {
|
|
543
|
+
answer,
|
|
544
|
+
followUpSuggestions: [],
|
|
545
|
+
diagrams: [],
|
|
546
|
+
generatedTests: [],
|
|
547
|
+
performanceHints: [],
|
|
548
|
+
metadata: {
|
|
549
|
+
intent: 'general',
|
|
550
|
+
processingTime: 0,
|
|
551
|
+
symbolsAnalyzed: 0,
|
|
552
|
+
filesAnalyzed: 0,
|
|
553
|
+
},
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
const enhancer = createResponseEnhancer(data.graph, data.symbols, fileContents);
|
|
557
|
+
// Если контекст есть - улучшаем ответ
|
|
558
|
+
if (smartContext) {
|
|
559
|
+
return enhancer.enhance(answer, smartContext);
|
|
560
|
+
}
|
|
561
|
+
// Fallback - минимальный ответ
|
|
562
|
+
return {
|
|
563
|
+
answer,
|
|
564
|
+
followUpSuggestions: [],
|
|
565
|
+
diagrams: [],
|
|
566
|
+
generatedTests: [],
|
|
567
|
+
performanceHints: [],
|
|
568
|
+
metadata: {
|
|
569
|
+
intent: 'general',
|
|
570
|
+
processingTime: 0,
|
|
571
|
+
symbolsAnalyzed: 0,
|
|
572
|
+
filesAnalyzed: 0,
|
|
573
|
+
},
|
|
574
|
+
};
|
|
575
|
+
}
|
|
502
576
|
async getStatistics(projectId) {
|
|
503
577
|
const project = this.projects.get(projectId);
|
|
504
578
|
if (!project) {
|
|
@@ -1080,5 +1154,26 @@ ${functions.slice(0, 30).map(f => `- \`${f.name}\` - ${sanitizePath(f.filePath)}
|
|
|
1080
1154
|
};
|
|
1081
1155
|
return renderTree(tree);
|
|
1082
1156
|
}
|
|
1157
|
+
/**
|
|
1158
|
+
* AI Narrator - человекопонятное описание архитектуры проекта
|
|
1159
|
+
*/
|
|
1160
|
+
async getNarrative(projectId, _language = 'ru') {
|
|
1161
|
+
const project = this.projects.get(projectId);
|
|
1162
|
+
if (!project) {
|
|
1163
|
+
throw new Error(`Project not found: ${projectId}`);
|
|
1164
|
+
}
|
|
1165
|
+
const data = await this.getProjectData(projectId);
|
|
1166
|
+
if (!data.graph || !data.symbols) {
|
|
1167
|
+
throw new Error('Project not indexed. Please index first via CLI.');
|
|
1168
|
+
}
|
|
1169
|
+
const fileContents = await this.getFileContents(projectId);
|
|
1170
|
+
// Анализируем стек технологий
|
|
1171
|
+
const { analyzeProjectStack } = await import('../../utils/project-analyzer.js');
|
|
1172
|
+
const projectMetadata = analyzeProjectStack(project.path);
|
|
1173
|
+
const narrator = new AINarrator();
|
|
1174
|
+
const report = await narrator.analyze(data.graph, data.symbols, fileContents, projectMetadata || undefined);
|
|
1175
|
+
Logger.success(`AI Narrator generated report for: ${project.name}`);
|
|
1176
|
+
return report;
|
|
1177
|
+
}
|
|
1083
1178
|
}
|
|
1084
1179
|
//# sourceMappingURL=project-service.js.map
|
package/dist/types/index.d.ts
CHANGED
|
@@ -233,6 +233,50 @@ export interface LLMContext {
|
|
|
233
233
|
recentChanges?: Change[];
|
|
234
234
|
language?: 'en' | 'ru';
|
|
235
235
|
projectMetadata?: ProjectMetadata;
|
|
236
|
+
smartContext?: {
|
|
237
|
+
intent: string;
|
|
238
|
+
query: string;
|
|
239
|
+
symbols: Array<{
|
|
240
|
+
name: string;
|
|
241
|
+
type: string;
|
|
242
|
+
file: string;
|
|
243
|
+
line: number;
|
|
244
|
+
snippet: string;
|
|
245
|
+
usedIn: Array<{
|
|
246
|
+
file: string;
|
|
247
|
+
line: number;
|
|
248
|
+
snippet: string;
|
|
249
|
+
}>;
|
|
250
|
+
calls: string[];
|
|
251
|
+
calledBy: string[];
|
|
252
|
+
confidence: number;
|
|
253
|
+
}>;
|
|
254
|
+
relevantFiles: Array<{
|
|
255
|
+
path: string;
|
|
256
|
+
relevance: number;
|
|
257
|
+
snippet?: string;
|
|
258
|
+
}>;
|
|
259
|
+
issues: Array<{
|
|
260
|
+
type: 'bug' | 'warning' | 'smell' | 'security';
|
|
261
|
+
severity: 'critical' | 'high' | 'medium' | 'low';
|
|
262
|
+
file: string;
|
|
263
|
+
line: number;
|
|
264
|
+
message: string;
|
|
265
|
+
snippet: string;
|
|
266
|
+
suggestion: string;
|
|
267
|
+
}>;
|
|
268
|
+
dependencies: Array<{
|
|
269
|
+
from: string;
|
|
270
|
+
to: string;
|
|
271
|
+
type: string;
|
|
272
|
+
}>;
|
|
273
|
+
projectStats: {
|
|
274
|
+
totalFiles: number;
|
|
275
|
+
totalSymbols: number;
|
|
276
|
+
languages: string[];
|
|
277
|
+
framework?: string;
|
|
278
|
+
};
|
|
279
|
+
};
|
|
236
280
|
}
|
|
237
281
|
export interface LLMResponse {
|
|
238
282
|
content: string;
|