AI-система управління технічним довгом
Технічний борг — це накопичені компроміси: "зробимо швидко зараз, перепишемо пізніше". Проблема в тому, що "пізніше" ніколи не наступає, тому що важко виміряти борг та обґрунтувати час на рефакторинг менеджменту. AI-система робить технічний борг вимірюваним, пріоритизованим і керованим як звичайний backlog.
Виявлення та вимірювання боргу
from anthropic import Anthropic
from pathlib import Path
import ast
import subprocess
import json
from dataclasses import dataclass, field
from typing import Literal
client = Anthropic()
@dataclass
class DebtItem:
id: str
file: str
category: Literal["code_smell", "architecture", "security", "test_coverage", "documentation", "dependency"]
severity: Literal["critical", "high", "medium", "low"]
title: str
description: str
estimated_hours: float
business_impact: str
quick_fix: bool = False
class TechDebtScanner:
def scan_project(self, project_root: str) -> list[DebtItem]:
"""Повне сканування технічного боргу проекту"""
all_items = []
# 1. Залежності з вразливостями
all_items.extend(self._scan_dependencies(project_root))
# 2. Складність коду
all_items.extend(self._scan_complexity(project_root))
# 3. AI-аналіз архітектурних проблем
all_items.extend(self._ai_scan_architecture(project_root))
# 4. TODO/FIXME/HACK коментарі
all_items.extend(self._scan_comments(project_root))
return all_items
def _scan_dependencies(self, project_root: str) -> list[DebtItem]:
"""Сканує залежності на вразливості та застарілі версії"""
items = []
# Safety для Python залежностей
result = subprocess.run(
["safety", "check", "--json", "--full-report"],
capture_output=True, text=True, cwd=project_root
)
if result.returncode != 0 and result.stdout:
try:
vulns = json.loads(result.stdout)
for vuln in vulns:
items.append(DebtItem(
id=f"dep_{vuln.get('package_name', 'unknown')}",
file="requirements.txt",
category="security",
severity="critical" if "critical" in str(vuln).lower() else "high",
title=f"Вразливість у {vuln.get('package_name')} {vuln.get('affected_versions')}",
description=vuln.get("vulnerability", ""),
estimated_hours=0.5,
business_impact="Потенційна вразливість безпеки",
quick_fix=True,
))
except json.JSONDecodeError:
pass
# pip-audit як альтернатива
result = subprocess.run(
["pip-audit", "--format=json"],
capture_output=True, text=True, cwd=project_root
)
# Обробка аналогічно...
return items
def _scan_complexity(self, project_root: str) -> list[DebtItem]:
"""Знаходить функції з високою циклічною складністю"""
items = []
result = subprocess.run(
["radon", "cc", "-j", "-n", "C", project_root], # Тільки оцінка C та вище
capture_output=True, text=True
)
if result.stdout:
data = json.loads(result.stdout)
for file_path, functions in data.items():
for func in functions:
complexity = func.get("complexity", 0)
if complexity >= 10:
hours = complexity * 0.5 # Груба оцінка
items.append(DebtItem(
id=f"cc_{file_path}_{func['name']}",
file=file_path,
category="code_smell",
severity="critical" if complexity >= 20 else "high" if complexity >= 15 else "medium",
title=f"Висока складність: {func['name']} (CC={complexity})",
description=f"Циклічна складність {complexity} перевищує поріг 10. Функція важко тестується та підтримується.",
estimated_hours=hours,
business_impact="Збільшує час змін, ризик багів при модифікації",
))
return items
def _scan_comments(self, project_root: str) -> list[DebtItem]:
"""Знаходить TODO/FIXME/HACK маркери"""
items = []
result = subprocess.run(
["grep", "-rn", "--include=*.py", r"#\s*\(TODO\|FIXME\|HACK\|XXX\|BUG\)", project_root],
capture_output=True, text=True
)
for line in result.stdout.splitlines():
if ":" in line:
parts = line.split(":", 2)
if len(parts) >= 3:
file_path, line_num, comment = parts
severity = "high" if "HACK" in comment or "FIXME" in comment else "low"
items.append(DebtItem(
id=f"todo_{hash(line)}",
file=file_path,
category="code_smell",
severity=severity,
title=f"Технічний маркер у коді",
description=comment.strip(),
estimated_hours=2.0,
business_impact="Задокументований технічний борг",
quick_fix=False,
))
return items
def _ai_scan_architecture(self, project_root: str) -> list[DebtItem]:
"""AI-аналіз архітектурних проблем"""
items = []
# Читаємо структуру проекту
structure = []
for root, dirs, files in Path(project_root).walk():
dirs[:] = [d for d in dirs if d not in {".git", "__pycache__", ".venv"}]
for f in files:
if f.endswith(".py"):
structure.append(str(Path(root) / f))
# Аналізуємо файли розміром > 500 рядків (потенційні God Objects)
large_files = []
for fp in structure[:50]:
try:
lines = Path(fp).read_text().splitlines()
if len(lines) > 500:
large_files.append((fp, len(lines)))
except Exception:
pass
if not large_files:
return items
response = client.messages.create(
model="claude-sonnet-4-5",
max_tokens=2048,
messages=[{
"role": "user",
"content": f"""Проаналізуй список великих файлів на архітектурні проблеми.
Файли (шлях, кількість рядків):
{json.dumps(large_files, ensure_ascii=False)}
Поверни JSON:
[{{
"file": "...",
"issue": "...",
"severity": "high|medium",
"estimated_hours": <число>,
"recommendation": "..."
}}]"""
}]
)
text = response.content[0].text
start = text.find("[")
end = text.rfind("]") + 1
arch_issues = json.loads(text[start:end])
for issue in arch_issues:
items.append(DebtItem(
id=f"arch_{hash(issue['file'])}",
file=issue["file"],
category="architecture",
severity=issue.get("severity", "medium"),
title=issue.get("issue", "Архітектурна проблема"),
description=issue.get("recommendation", ""),
estimated_hours=issue.get("estimated_hours", 8.0),
business_impact="Уповільнює розробку нових фіч",
))
return items
Пріоритизація та планування
class TechDebtPlanner:
def prioritize(
self,
items: list[DebtItem],
available_hours: float,
team_velocity: float = 0.7,
) -> dict:
"""Пріоритизує борг з урахуванням доступного часу"""
# Скор = impact * urgency / effort
severity_weights = {"critical": 100, "high": 40, "medium": 10, "low": 2}
scored = []
for item in items:
base_score = severity_weights[item.severity]
effort = max(item.estimated_hours, 0.5)
# Quick fixes в пріоритеті
if item.quick_fix:
base_score *= 2
score = base_score / effort
scored.append((score, item))
scored.sort(key=lambda x: x[0], reverse=True)
# Формуємо план
selected = []
total_hours = 0.0
effective_hours = available_hours * team_velocity
for _, item in scored:
if total_hours + item.estimated_hours <= effective_hours:
selected.append(item)
total_hours += item.estimated_hours
return {
"selected_items": selected,
"total_hours": total_hours,
"debt_reduced_hours": total_hours,
"remaining_items": [item for _, item in scored if item not in selected],
"sprint_capacity": available_hours,
}
def generate_jira_tickets(self, items: list[DebtItem]) -> list[dict]:
"""Генерує Jira-задачі для технічного боргу"""
response = client.messages.create(
model="claude-sonnet-4-5",
max_tokens=4096,
messages=[{
"role": "user",
"content": f"""Сформуй Jira-задачі для технічного боргу.
Елементи боргу:
{json.dumps([{
"title": i.title,
"description": i.description,
"severity": i.severity,
"hours": i.estimated_hours,
"business_impact": i.business_impact,
} for i in items], ensure_ascii=False, indent=2)}
Для кожного створи Jira-задачу:
{{
"summary": "...",
"description": "...",
"story_points": <1-13>,
"priority": "Highest|High|Medium|Low",
"labels": ["tech-debt", "<category>"],
"acceptance_criteria": ["..."]
}}"""
}]
)
text = response.content[0].text
start = text.find("[")
end = text.rfind("]") + 1
return json.loads(text[start:end])
Дашборд технічного боргу
def generate_debt_dashboard(project_root: str) -> dict:
"""Генерує повний звіт по технічному борзі"""
scanner = TechDebtScanner()
items = scanner.scan_project(project_root)
by_severity = {}
for item in items:
by_severity.setdefault(item.severity, []).append(item)
total_hours = sum(i.estimated_hours for i in items)
return {
"total_items": len(items),
"total_hours": total_hours,
"debt_index": round(total_hours / max(len(list(Path(project_root).rglob("*.py"))), 1), 2),
"by_severity": {k: len(v) for k, v in by_severity.items()},
"by_category": {},
"top_10_critical": sorted(
[i for i in items if i.severity in ("critical", "high")],
key=lambda x: x.estimated_hours,
reverse=True
)[:10],
}
Практичний кейс: SaaS з 4-річним довгом
Контекст: SaaS для HR, 4 роки розробки, 3 зміни команди. Скарги: будь-яка нова фіча займає в 3–4 рази довше очікуваного.
Результати сканування:
- 847 елементів технічного боргу
- Критичних: 23 (12 вразливостей у залежностях, 11 God Objects)
- Загальна оцінка: 1340 годин для повного погашення
- Debt Index: 8.7 (високий — норма < 3.0)
План дій:
- Sprint 1 (20г): всі вразливості залежностей — оновлення пакетів, 0.5г кожна
- Sprint 2–4 (60г): 4 головні God Object → декомпозиція
- Sprint 5–8 (80г): test coverage з 28% → 70%
Результати за 4 місяці:
- Debt Index: 8.7 → 3.2
- Час реалізації типової фіч: -41%
- Bug rate у production: -38%
Розрахунок ROI: 160 годин на погашення боргу зберегли ~320 годин уповільнення за квартал — окупаємість 2:1 за перший квартал.
Терміни
- Базовий сканер (complexity + TODO + залежності): 3–5 днів
- AI-аналіз архітектурних проблем: 1 тиждень
- Пріоритизація + генерація Jira-задач: 1 тиждень
- Dashboard з історичними трендами: 2 тижні







