Skip to content

Commit 5aa24a3

Browse files
committed
feat: Phase 3 — Bug Correlation + Relation Extraction
Новые модули: - bug_correlation.py: анализ баго-нагрузки, hotspots, risk levels - relation_extractor.py: cochange/bug/call relations, knowledge graph MCP tools: - get_bug_correlation: анализ багов по проекту или файлу - get_related_files: поиск связанных файлов через knowledge graph Тесты: 15 новых (всего 219) Phase 3: 3/4 выполнено
1 parent c3978dc commit 5aa24a3

7 files changed

Lines changed: 1118 additions & 5 deletions

File tree

‎AGENT_DIARY.md‎

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,53 @@
22

33
---
44

5-
## [2026-06-28 23:30] — [Type: Feature] — Impact Analysis MCP Tool (Phase 1)
5+
## [2026-06-29 14:00] — [Type: Feature] — Phase 3: Bug Correlation + Relation Extraction
6+
7+
**Проблема:**
8+
- Нет инструментов для анализа баго-нагрузки файлов
9+
- Нет автоматического извлечения связей между файлами
10+
- Пользователь не знает какие файлы "горячие" и чаще всего ломаются
11+
12+
**Решение:**
13+
- Создан `src/core/bug_correlation.py` — BugCorrelation класс:
14+
- Определение баг-фиксов по ключевым словам (fix, bug, hotfix, resolve, error, crash...)
15+
- Топ проблемных файлов (top_buggy_files)
16+
- Горячие точки (hotspots) с учётом давности баг-фиксов
17+
- Баго-история для файла/символа с уровнем риска (low/medium/high/critical)
18+
- Создан `src/core/relation_extractor.py` — RelationExtractor класс:
19+
- Cochange relations — файлы меняющиеся вместе
20+
- Bug correlations — файлы связанные через баг-фиксы
21+
- Call relations — вызовы из SymbolIndex
22+
- Построение графа знаний (build_knowledge_graph)
23+
- Поиск связанных файлов с глубиной (depth 1-2)
24+
- Добавлены 2 новых MCP tool:
25+
- `get_bug_correlation` — анализ баго-корреляции
26+
- `get_related_files` — поиск связанных файлов через knowledge graph
27+
- 15 новых тестов (все проходят)
28+
- Обновлён VISION.md: Phase 3 IN PROGRESS
29+
30+
**Инструменты:** write_file, edit_file, terminal (pytest), diagnostics, search_code
31+
**Файлы:** src/core/bug_correlation.py, src/core/relation_extractor.py, src/mcp/server.py, tests/test_bug_correlation.py, tests/test_relation_extractor.py, VISION.md
32+
**Статус:** ✅ 219 тестов пройдено (было 204, +15)
33+
34+
---
35+
36+
## [2026-06-29 13:30] — [Type: Feature] — watcher_status: автозагрузка моделей LM Studio
37+
38+
**Проблема:**
39+
- Пользователь забывает загрузить модели в LM Studio
40+
- Первый поиск может быть долгим из-за загрузки модели по требованию
41+
42+
**Решение:**
43+
- Добавлена проверка состояния моделей через `/api/v0/models`
44+
- Показ загруженных/незагруженных моделей в VRAM
45+
- Автоматическая warmup-загрузка если embedding/instruct не в памяти
46+
- Загрузка embedding через `POST /v1/embeddings` с "warmup"
47+
- Загрузка instruct через `POST /v1/chat/completions` с "hi"
48+
49+
**Инструменты:** edit_file, watcher_status
50+
**Файлы:** src/mcp/server.py
51+
**Статус:** ✅ Закоммичено и запушено (c3978dc)
652

753
**Проблема:**
854
- Нет инструмента для оценки риска изменения/удаления символа

‎VISION.md‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -63,11 +63,12 @@
6363
- [ ] **RepoRank** — PageRank на графе вызовов для приоритизации
6464
- [ ] **Branch-aware индекс** — разные индексы для разных веток
6565

66-
### Phase 3: Code Memory (目标: 92%)
66+
### Phase 3: Code Memory (目标: 92%) — IN PROGRESS
6767

68-
- [ ] **Semantic commit memory** — история изменений с контекстом
69-
- [ ] **Автоматические связи** — "эта функция чаще всего меняется вместе с X"
70-
- [ ] **Bug correlation** — какие файлы чаще участвуют в багах
68+
- [x] **Semantic commit memory** — история изменений с контекстом (Phase 2.4)
69+
- [x] **Bug correlation** — какие файлы чаще участвуют в багах
70+
- [x] **Auto relation extraction** — cochange + bug + call relations → knowledge graph
71+
- [ ] **GraphRAG query engine** — навигация по графу знаний
7172

7273
### Phase 4: Full GraphRAG (目标: 95%)
7374

‎src/core/bug_correlation.py‎

Lines changed: 262 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
1+
"""
2+
Bug Correlation — анализ связи багов с изменениями в коде.
3+
4+
Связывает:
5+
- Коммиты с ключевыми словами (fix, bug, hotfix, resolve) → "баг-фиксы"
6+
- Баг-фиксы с файлами и символами
7+
- Символы с "баго-историей" — сколько раз их правили из-за багов
8+
9+
Позволяет отвечать на вопросы:
10+
- "Какие модули чаще всего ломаются?"
11+
- "Какие файлы имеют наибольшую баго-нагрузку?"
12+
- "Кто чаще всего правит этот модуль?"
13+
"""
14+
15+
import logging
16+
import re
17+
from collections import defaultdict
18+
from datetime import datetime, timedelta
19+
from pathlib import Path
20+
from typing import Dict, List, Optional, Any
21+
22+
logger = logging.getLogger("bug_correlation")
23+
24+
# Паттерны для определения баг-фиксов
25+
BUGFIX_PATTERNS = [
26+
r'\bfix\b',
27+
r'\bbug\b',
28+
r'\bhotfix\b',
29+
r'\bresolve\b',
30+
r'\bpatch\b',
31+
r'\bworkaround\b',
32+
r'\bcorrect\b',
33+
r'\bissue\b',
34+
r'\bdefect\b',
35+
r'\bcrash\b',
36+
r'\berror\b',
37+
r'\bbroken\b',
38+
]
39+
40+
BUGFIX_REGEX = re.compile('|'.join(BUGFIX_PATTERNS), re.IGNORECASE)
41+
42+
43+
class BugCorrelation:
44+
"""Анализ связи багов с изменениями в коде."""
45+
46+
def __init__(self, commit_memory):
47+
"""
48+
Args:
49+
commit_memory: Инстанс CommitMemory с загруженными коммитами
50+
"""
51+
self.commit_memory = commit_memory
52+
self._bug_commits: List[Dict] = []
53+
self._file_bug_count: Dict[str, int] = {}
54+
self._symbol_bug_count: Dict[str, int] = {}
55+
self._analyzed = False
56+
57+
def analyze(self) -> Dict[str, Any]:
58+
"""Полный анализ баго-корреляции.
59+
60+
Returns:
61+
Результаты анализа: баг-коммиты, статистика по файлам/символам
62+
"""
63+
commits = self.commit_memory._commits
64+
if not commits:
65+
self.commit_memory.fetch_commits()
66+
commits = self.commit_memory._commits
67+
68+
self._bug_commits = []
69+
self._file_bug_count = defaultdict(int)
70+
self._symbol_bug_count = defaultdict(int)
71+
72+
for commit in commits:
73+
message = commit.get("message", "")
74+
if self._is_bugfix(message):
75+
self._bug_commits.append(commit)
76+
# Считаем баги по файлам
77+
for f in commit.get("files", []):
78+
self._file_bug_count[f] += 1
79+
80+
self._analyzed = True
81+
82+
return {
83+
"total_commits": len(commits),
84+
"bugfix_commits": len(self._bug_commits),
85+
"bugfix_ratio": round(len(self._bug_commits) / max(len(commits), 1), 3),
86+
"top_buggy_files": self.get_top_buggy_files(10),
87+
}
88+
89+
def _is_bugfix(self, message: str) -> bool:
90+
"""Определяет является ли коммит баг-фиксом."""
91+
return bool(BUGFIX_REGEX.search(message))
92+
93+
def get_bugfix_commits(self, limit: int = 50) -> List[Dict]:
94+
"""Возвращает список баг-фикс коммитов."""
95+
if not self._analyzed:
96+
self.analyze()
97+
return self._bug_commits[:limit]
98+
99+
def get_top_buggy_files(self, top_n: int = 10) -> List[Dict]:
100+
"""Возвращает файлы с наибольшим количеством баг-фиксов.
101+
102+
Returns:
103+
[{file, bug_count, total_commits, bug_ratio}]
104+
"""
105+
if not self._analyzed:
106+
self.analyze()
107+
108+
# Считаем общее количество коммитов по файлам
109+
file_total = defaultdict(int)
110+
for commit in self.commit_memory._commits:
111+
for f in commit.get("files", []):
112+
file_total[f] += 1
113+
114+
# Сортируем по количеству багов
115+
sorted_files = sorted(
116+
self._file_bug_count.items(),
117+
key=lambda x: x[1],
118+
reverse=True
119+
)[:top_n]
120+
121+
result = []
122+
for file_path, bug_count in sorted_files:
123+
total = file_total.get(file_path, 0)
124+
result.append({
125+
"file": file_path,
126+
"bug_count": bug_count,
127+
"total_commits": total,
128+
"bug_ratio": round(bug_count / max(total, 1), 3),
129+
})
130+
131+
return result
132+
133+
def get_bug_history_for_file(self, file_path: str) -> Dict:
134+
"""Получает полную баго-историю файла.
135+
136+
Args:
137+
file_path: Путь к файлу
138+
139+
Returns:
140+
{file, bug_commits, total_commits, bug_risk}
141+
"""
142+
if not self._analyzed:
143+
self.analyze()
144+
145+
file_commits = self.commit_memory.get_commits_for_file(file_path)
146+
bug_commits = [c for c in file_commits if self._is_bugfix(c.get("message", ""))]
147+
148+
# Определяем уровень риска
149+
total = len(file_commits)
150+
bug_count = len(bug_commits)
151+
ratio = bug_count / max(total, 1)
152+
153+
if ratio >= 0.5:
154+
risk = "critical"
155+
elif ratio >= 0.3:
156+
risk = "high"
157+
elif ratio >= 0.1:
158+
risk = "medium"
159+
else:
160+
risk = "low"
161+
162+
return {
163+
"file": file_path,
164+
"bug_commits": bug_commits,
165+
"total_commits": total,
166+
"bug_count": bug_count,
167+
"bug_ratio": round(ratio, 3),
168+
"bug_risk": risk,
169+
}
170+
171+
def get_bug_history_for_symbol(self, symbol_name: str, file_path: str = "") -> Dict:
172+
"""Получает баго-историю для символа.
173+
174+
Args:
175+
symbol_name: Имя символа
176+
file_path: Файл где символ определён
177+
178+
Returns:
179+
{symbol, file, bug_commits, risk}
180+
"""
181+
if not self._analyzed:
182+
self.analyze()
183+
184+
# Ищем коммиты связанные с символом
185+
symbol_commits = self.commit_memory.get_commits_for_symbol(symbol_name, file_path)
186+
bug_commits = [c for c in symbol_commits if self._is_bugfix(c.get("message", ""))]
187+
188+
return {
189+
"symbol": symbol_name,
190+
"file": file_path,
191+
"bug_commits": bug_commits,
192+
"bug_count": len(bug_commits),
193+
"total_commits": len(symbol_commits),
194+
"bug_risk": "high" if len(bug_commits) >= 3 else "medium" if len(bug_commits) >= 1 else "low",
195+
}
196+
197+
def get_hotspots(self, top_n: int = 10) -> List[Dict]:
198+
"""Находит "горячие точки" — файлы с высокой баго-нагрузкой.
199+
200+
Учитывает:
201+
- Количество баг-фиксов
202+
- Давность последнего баг-фикса
203+
- Частоту изменений
204+
205+
Returns:
206+
[{file, bug_score, last_bugfix, risk}]
207+
"""
208+
if not self._analyzed:
209+
self.analyze()
210+
211+
hotspots = []
212+
now = datetime.now()
213+
214+
for file_path, bug_count in self._file_bug_count.items():
215+
# Находим последний баг-фикс
216+
file_commits = self.commit_memory.get_commits_for_file(file_path)
217+
bug_commits = [c for c in file_commits if self._is_bugfix(c.get("message", ""))]
218+
219+
last_bugfix = None
220+
if bug_commits:
221+
dates = [c.get("date", "") for c in bug_commits if c.get("date")]
222+
if dates:
223+
last_bugfix = max(dates)
224+
225+
# Считаем score: чем больше багов и чем свежее — тем выше
226+
recency_bonus = 0
227+
if last_bugfix:
228+
try:
229+
bug_date = datetime.fromisoformat(last_bugfix)
230+
days_ago = (now - bug_date).days
231+
if days_ago < 30:
232+
recency_bonus = 3
233+
elif days_ago < 90:
234+
recency_bonus = 1
235+
except (ValueError, TypeError):
236+
pass
237+
238+
score = bug_count * 2 + recency_bonus
239+
240+
hotspots.append({
241+
"file": file_path,
242+
"bug_count": bug_count,
243+
"bug_score": score,
244+
"last_bugfix": last_bugfix,
245+
"risk": "critical" if score >= 10 else "high" if score >= 5 else "medium",
246+
})
247+
248+
hotspots.sort(key=lambda x: x["bug_score"], reverse=True)
249+
return hotspots[:top_n]
250+
251+
def get_stats(self) -> Dict:
252+
"""Статистика баго-корреляции."""
253+
if not self._analyzed:
254+
self.analyze()
255+
256+
return {
257+
"total_commits": len(self.commit_memory._commits),
258+
"bugfix_commits": len(self._bug_commits),
259+
"bugfix_ratio": round(len(self._bug_commits) / max(len(self.commit_memory._commits), 1), 3),
260+
"unique_buggy_files": len(self._file_bug_count),
261+
"top_risk_file": self.get_top_buggy_files(1)[0]["file"] if self._file_bug_count else None,
262+
}

0 commit comments

Comments
 (0)