codedna 0.2.0__py3-none-any.whl
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.
- codedna/__init__.py +4 -0
- codedna/ai_fingerprint.py +223 -0
- codedna/analyzer.py +245 -0
- codedna/api.py +1505 -0
- codedna/auth.py +372 -0
- codedna/bus_factor.py +259 -0
- codedna/cli.py +1965 -0
- codedna/db.py +336 -0
- codedna/git_hook.py +212 -0
- codedna/integrations/__init__.py +1 -0
- codedna/integrations/github_bot.py +259 -0
- codedna/integrations/jira.py +166 -0
- codedna/integrations/lemonsqueezy.py +236 -0
- codedna/interview.py +298 -0
- codedna/onboarding.py +195 -0
- codedna/plan.py +184 -0
- codedna/protection.py +211 -0
- codedna/rate_limit.py +83 -0
- codedna/scorer.py +221 -0
- codedna/sprint_health.py +187 -0
- codedna/survey.py +104 -0
- codedna/tech_debt.py +232 -0
- codedna-0.2.0.dist-info/METADATA +93 -0
- codedna-0.2.0.dist-info/RECORD +26 -0
- codedna-0.2.0.dist-info/WHEEL +4 -0
- codedna-0.2.0.dist-info/entry_points.txt +2 -0
codedna/protection.py
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
"""Kritik modüller için anlama eşiği izleme — kod sahipliği sigortası."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import time
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
from codedna.db import get_connection
|
|
11
|
+
|
|
12
|
+
# Durum sabitleri
|
|
13
|
+
_DURUM_IHLAL = "İHLAL"
|
|
14
|
+
_DURUM_GUVENLI = "GÜVENLİ"
|
|
15
|
+
_DURUM_BILINMIYOR = "BİLİNMİYOR"
|
|
16
|
+
|
|
17
|
+
# Aynı modül için uyarılar arası minimum süre (spam önleme, saniye)
|
|
18
|
+
_MIN_UYARI_ARASI = 3600 # 1 saat
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class ModulDurumu:
|
|
23
|
+
"""Tek korumalı modülün anlık durumu."""
|
|
24
|
+
|
|
25
|
+
dosya_yolu: str
|
|
26
|
+
etiket: str
|
|
27
|
+
esik: float
|
|
28
|
+
mevcut_skor: Optional[float]
|
|
29
|
+
durum: str # İHLAL / GÜVENLİ / BİLİNMİYOR
|
|
30
|
+
aktif: bool
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def protect_module(
|
|
34
|
+
file_path: str,
|
|
35
|
+
threshold: float,
|
|
36
|
+
label: str,
|
|
37
|
+
author: str,
|
|
38
|
+
db_path: Path,
|
|
39
|
+
) -> int:
|
|
40
|
+
"""
|
|
41
|
+
Bir dosyayı korumalı modül olarak işaretle.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
file_path: Korunacak dosyanın yolu
|
|
45
|
+
threshold: Minimum anlama skoru eşiği (1.0–5.0)
|
|
46
|
+
label: İnsan okunabilir etiket (örn. "Ödeme Sistemi")
|
|
47
|
+
author: Korumayı ekleyen kişi
|
|
48
|
+
db_path: SQLite veritabanı yolu
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
Yeni kaydın id'si
|
|
52
|
+
"""
|
|
53
|
+
with get_connection(db_path) as conn:
|
|
54
|
+
cur = conn.execute(
|
|
55
|
+
"""
|
|
56
|
+
INSERT INTO protected_modules
|
|
57
|
+
(file_path, min_understanding_threshold, label, added_by, added_at, is_active)
|
|
58
|
+
VALUES (?, ?, ?, ?, ?, 1)
|
|
59
|
+
ON CONFLICT(file_path) DO UPDATE SET
|
|
60
|
+
min_understanding_threshold = excluded.min_understanding_threshold,
|
|
61
|
+
label = excluded.label,
|
|
62
|
+
added_by = excluded.added_by,
|
|
63
|
+
added_at = excluded.added_at,
|
|
64
|
+
is_active = 1
|
|
65
|
+
""",
|
|
66
|
+
(file_path, threshold, label, author, int(time.time())),
|
|
67
|
+
)
|
|
68
|
+
return cur.lastrowid or 0
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def unprotect_module(file_path: str, db_path: Path) -> bool:
|
|
72
|
+
"""
|
|
73
|
+
Korumayı kaldır (is_active = 0 yap, kaydı silme).
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
file_path: Koruma kaldırılacak dosya yolu
|
|
77
|
+
db_path: SQLite veritabanı yolu
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
Kayıt bulunup güncellendiyse True
|
|
81
|
+
"""
|
|
82
|
+
with get_connection(db_path) as conn:
|
|
83
|
+
cur = conn.execute(
|
|
84
|
+
"UPDATE protected_modules SET is_active = 0 WHERE file_path = ?",
|
|
85
|
+
(file_path,),
|
|
86
|
+
)
|
|
87
|
+
return cur.rowcount > 0
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _dosya_mevcut_skoru(file_path: str, db_path: Path) -> Optional[float]:
|
|
91
|
+
"""DB'den dosyanın en güncel anlama skorunu çek."""
|
|
92
|
+
try:
|
|
93
|
+
with get_connection(db_path) as conn:
|
|
94
|
+
row = conn.execute(
|
|
95
|
+
"""
|
|
96
|
+
SELECT fs.understanding_score
|
|
97
|
+
FROM file_scores fs
|
|
98
|
+
JOIN commits c ON fs.commit_hash = c.commit_hash
|
|
99
|
+
WHERE fs.file_path = ? AND fs.understanding_score IS NOT NULL
|
|
100
|
+
ORDER BY c.timestamp DESC
|
|
101
|
+
LIMIT 1
|
|
102
|
+
""",
|
|
103
|
+
(file_path,),
|
|
104
|
+
).fetchone()
|
|
105
|
+
return float(row["understanding_score"]) if row else None
|
|
106
|
+
except Exception:
|
|
107
|
+
return None
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def check_protected_modules(db_path: Path) -> list[ModulDurumu]:
|
|
111
|
+
"""
|
|
112
|
+
Tüm aktif korumalı modülleri kontrol et, anlama eşiğini aşıp aşmadığını belirle.
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
ModulDurumu listesi
|
|
116
|
+
"""
|
|
117
|
+
try:
|
|
118
|
+
with get_connection(db_path) as conn:
|
|
119
|
+
kayitlar = conn.execute(
|
|
120
|
+
"""
|
|
121
|
+
SELECT file_path, min_understanding_threshold, label, is_active
|
|
122
|
+
FROM protected_modules
|
|
123
|
+
WHERE is_active = 1
|
|
124
|
+
ORDER BY label
|
|
125
|
+
"""
|
|
126
|
+
).fetchall()
|
|
127
|
+
except Exception:
|
|
128
|
+
return []
|
|
129
|
+
|
|
130
|
+
sonuclar: list[ModulDurumu] = []
|
|
131
|
+
for k in kayitlar:
|
|
132
|
+
mevcut = _dosya_mevcut_skoru(k["file_path"], db_path)
|
|
133
|
+
|
|
134
|
+
if mevcut is None:
|
|
135
|
+
durum = _DURUM_BILINMIYOR
|
|
136
|
+
elif mevcut >= k["min_understanding_threshold"]:
|
|
137
|
+
durum = _DURUM_GUVENLI
|
|
138
|
+
else:
|
|
139
|
+
durum = _DURUM_IHLAL
|
|
140
|
+
|
|
141
|
+
sonuclar.append(
|
|
142
|
+
ModulDurumu(
|
|
143
|
+
dosya_yolu=k["file_path"],
|
|
144
|
+
etiket=k["label"] or k["file_path"],
|
|
145
|
+
esik=k["min_understanding_threshold"],
|
|
146
|
+
mevcut_skor=round(mevcut, 2) if mevcut is not None else None,
|
|
147
|
+
durum=durum,
|
|
148
|
+
aktif=bool(k["is_active"]),
|
|
149
|
+
)
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
return sonuclar
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def get_violations(db_path: Path) -> list[ModulDurumu]:
|
|
156
|
+
"""
|
|
157
|
+
Sadece eşik altına düşmüş (İHLAL durumundaki) modülleri döndür.
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
İhlaldeki ModulDurumu listesi
|
|
161
|
+
"""
|
|
162
|
+
return [m for m in check_protected_modules(db_path) if m.durum == _DURUM_IHLAL]
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def ihlal_uyarisi_goster(db_path: Path) -> list[str]:
|
|
166
|
+
"""
|
|
167
|
+
Post-commit hook için ihlal uyarılarını döndür.
|
|
168
|
+
Spam önlemek için son 1 saat içinde uyarı verilen modülleri atla.
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
Uyarı mesajı listesi (boşsa ihlal yok)
|
|
172
|
+
"""
|
|
173
|
+
ihlaller = get_violations(db_path)
|
|
174
|
+
if not ihlaller:
|
|
175
|
+
return []
|
|
176
|
+
|
|
177
|
+
su_an = int(time.time())
|
|
178
|
+
uyarilar: list[str] = []
|
|
179
|
+
|
|
180
|
+
for ihlal in ihlaller:
|
|
181
|
+
# Son uyarı zamanını kontrol et
|
|
182
|
+
try:
|
|
183
|
+
with get_connection(db_path) as conn:
|
|
184
|
+
row = conn.execute(
|
|
185
|
+
"SELECT last_alert_at FROM protected_modules WHERE file_path = ?",
|
|
186
|
+
(ihlal.dosya_yolu,),
|
|
187
|
+
).fetchone()
|
|
188
|
+
son_uyari = row["last_alert_at"] if row and row["last_alert_at"] else 0
|
|
189
|
+
except Exception:
|
|
190
|
+
son_uyari = 0
|
|
191
|
+
|
|
192
|
+
if su_an - son_uyari < _MIN_UYARI_ARASI:
|
|
193
|
+
continue
|
|
194
|
+
|
|
195
|
+
# Uyarı zamanını güncelle
|
|
196
|
+
try:
|
|
197
|
+
with get_connection(db_path) as conn:
|
|
198
|
+
conn.execute(
|
|
199
|
+
"UPDATE protected_modules SET last_alert_at = ? WHERE file_path = ?",
|
|
200
|
+
(su_an, ihlal.dosya_yolu),
|
|
201
|
+
)
|
|
202
|
+
except Exception:
|
|
203
|
+
pass
|
|
204
|
+
|
|
205
|
+
skor_str = f"{ihlal.mevcut_skor:.1f}" if ihlal.mevcut_skor else "?"
|
|
206
|
+
uyarilar.append(
|
|
207
|
+
f"⚠️ UYARI: {ihlal.dosya_yolu} artık güvenle değiştirilemiyor "
|
|
208
|
+
f"(anlama: {skor_str} < eşik: {ihlal.esik:.1f})"
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
return uyarilar
|
codedna/rate_limit.py
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Basit in-memory rate limiter — brute-force koruması için.
|
|
3
|
+
|
|
4
|
+
Production-grade değil (çok process ortamında çalışmaz, sunucu yeniden
|
|
5
|
+
başlatıldığında sıfırlanır). Temel bir koruma katmanı sağlar.
|
|
6
|
+
Production için Redis tabanlı bir çözüm tercih edilmeli.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import time
|
|
12
|
+
from collections import defaultdict
|
|
13
|
+
from threading import Lock
|
|
14
|
+
from typing import Optional
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# Yapılandırma
|
|
18
|
+
_MAX_DENEME = 5 # Bu süre içinde maksimum başarısız deneme
|
|
19
|
+
_PENCERE_SANIYE = 300 # 5 dakikalık pencere
|
|
20
|
+
_KILIT_SANIYE = 60 # Kilitlenme süresi (saniye)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class RateLimiter:
|
|
24
|
+
"""Thread-safe in-memory rate limiter."""
|
|
25
|
+
|
|
26
|
+
def __init__(
|
|
27
|
+
self,
|
|
28
|
+
max_deneme: int = _MAX_DENEME,
|
|
29
|
+
pencere: int = _PENCERE_SANIYE,
|
|
30
|
+
kilit: int = _KILIT_SANIYE,
|
|
31
|
+
) -> None:
|
|
32
|
+
self._max_deneme = max_deneme
|
|
33
|
+
self._pencere = pencere
|
|
34
|
+
self._kilit = kilit
|
|
35
|
+
# {anahtar: [(timestamp, basarisiz_mi), ...]}
|
|
36
|
+
self._kayitlar: dict[str, list[tuple[int, bool]]] = defaultdict(list)
|
|
37
|
+
self._kilitler: dict[str, int] = {} # {anahtar: kilit_bitis_zamani}
|
|
38
|
+
self._lock = Lock()
|
|
39
|
+
|
|
40
|
+
def kontrol_et(self, anahtar: str) -> tuple[bool, Optional[int]]:
|
|
41
|
+
"""
|
|
42
|
+
İsteğe izin verip vermeyeceğini kontrol et.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
anahtar: IP adresi veya e-posta gibi tanımlayıcı
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
(izin_var, kalan_saniye) — kilitliyse kalan_saniye > 0
|
|
49
|
+
"""
|
|
50
|
+
su_an = int(time.time())
|
|
51
|
+
with self._lock:
|
|
52
|
+
# Kilit kontrolü
|
|
53
|
+
kilit_bitis = self._kilitler.get(anahtar, 0)
|
|
54
|
+
if su_an < kilit_bitis:
|
|
55
|
+
return False, kilit_bitis - su_an
|
|
56
|
+
|
|
57
|
+
# Eski kayıtları temizle
|
|
58
|
+
pencere_basi = su_an - self._pencere
|
|
59
|
+
self._kayitlar[anahtar] = [
|
|
60
|
+
(ts, basarisiz)
|
|
61
|
+
for ts, basarisiz in self._kayitlar[anahtar]
|
|
62
|
+
if ts > pencere_basi
|
|
63
|
+
]
|
|
64
|
+
return True, None
|
|
65
|
+
|
|
66
|
+
def basarisiz_kaydet(self, anahtar: str) -> None:
|
|
67
|
+
"""Başarısız denemeyi kaydet, gerekirse kilitle."""
|
|
68
|
+
su_an = int(time.time())
|
|
69
|
+
with self._lock:
|
|
70
|
+
self._kayitlar[anahtar].append((su_an, True))
|
|
71
|
+
basarisiz = sum(1 for _, b in self._kayitlar[anahtar] if b)
|
|
72
|
+
if basarisiz >= self._max_deneme:
|
|
73
|
+
self._kilitler[anahtar] = su_an + self._kilit
|
|
74
|
+
|
|
75
|
+
def basarili_kaydet(self, anahtar: str) -> None:
|
|
76
|
+
"""Başarılı girişte kaydı sıfırla."""
|
|
77
|
+
with self._lock:
|
|
78
|
+
self._kayitlar[anahtar] = []
|
|
79
|
+
self._kilitler.pop(anahtar, None)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
# Global limiter instance — login endpoint'i için
|
|
83
|
+
login_limiter = RateLimiter()
|
codedna/scorer.py
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
"""Git geçmişinden single_commit_ratio hesaplama ve commit skorlama."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
from git import InvalidGitRepositoryError, Repo
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
|
|
11
|
+
from codedna.analyzer import FileAnalysisResult, analyze_file
|
|
12
|
+
|
|
13
|
+
console = Console()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def get_repo(path: Optional[Path] = None) -> Optional[Repo]:
|
|
17
|
+
"""Git repo nesnesini döndür, bulunamazsa None."""
|
|
18
|
+
try:
|
|
19
|
+
return Repo(path or Path.cwd(), search_parent_directories=True)
|
|
20
|
+
except InvalidGitRepositoryError:
|
|
21
|
+
return None
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def calculate_single_commit_ratio(repo: Repo, file_path: str) -> float:
|
|
25
|
+
"""
|
|
26
|
+
Dosyanın kaç satırının tek bir commit'te eklendiğini hesapla.
|
|
27
|
+
|
|
28
|
+
Stratejisi: Dosyanın geçmiş commit'lerinde en büyük tek commit
|
|
29
|
+
katkısının toplam satıra oranını döndür.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
repo: Git repo nesnesi
|
|
33
|
+
file_path: Repo köküne göre dosya yolu
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
0.0 ile 1.0 arasında oran
|
|
37
|
+
"""
|
|
38
|
+
try:
|
|
39
|
+
# Dosyayla ilgili commit geçmişini al
|
|
40
|
+
commitler = list(repo.iter_commits(paths=file_path, max_count=50))
|
|
41
|
+
if not commitler:
|
|
42
|
+
return 0.0
|
|
43
|
+
|
|
44
|
+
# Her commit'teki satır ekleme sayısını topla
|
|
45
|
+
commit_katkilar: list[int] = []
|
|
46
|
+
for commit in commitler:
|
|
47
|
+
try:
|
|
48
|
+
if commit.parents:
|
|
49
|
+
diff = commit.parents[0].diff(commit, paths=[file_path])
|
|
50
|
+
else:
|
|
51
|
+
# İlk commit
|
|
52
|
+
diff = commit.diff(None, paths=[file_path])
|
|
53
|
+
|
|
54
|
+
for d in diff:
|
|
55
|
+
if d.a_blob or d.b_blob:
|
|
56
|
+
# Eklenen satır sayısını tahmin et
|
|
57
|
+
try:
|
|
58
|
+
stats = commit.stats.files.get(file_path, {})
|
|
59
|
+
eklenen = stats.get("insertions", 0)
|
|
60
|
+
commit_katkilar.append(eklenen)
|
|
61
|
+
except Exception:
|
|
62
|
+
pass
|
|
63
|
+
except Exception:
|
|
64
|
+
pass
|
|
65
|
+
|
|
66
|
+
if not commit_katkilar:
|
|
67
|
+
# Fallback: tek commit varsa %100 say
|
|
68
|
+
return 1.0 if len(commitler) == 1 else 0.5
|
|
69
|
+
|
|
70
|
+
toplam = sum(commit_katkilar)
|
|
71
|
+
if toplam == 0:
|
|
72
|
+
return 0.0
|
|
73
|
+
|
|
74
|
+
en_buyuk = max(commit_katkilar)
|
|
75
|
+
return min(en_buyuk / toplam, 1.0)
|
|
76
|
+
|
|
77
|
+
except Exception:
|
|
78
|
+
return 0.0
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def scan_repository(
|
|
82
|
+
repo_path: Optional[Path] = None,
|
|
83
|
+
max_files: int = 200,
|
|
84
|
+
) -> list[FileAnalysisResult]:
|
|
85
|
+
"""
|
|
86
|
+
Repo'daki desteklenen tüm dosyaları tara.
|
|
87
|
+
|
|
88
|
+
Önemli: Git reposu varsa SADECE git'in izlediği dosyalar taranır.
|
|
89
|
+
Bu sayede .gitignore'daki build çıktıları (`.next/`, `out/`,
|
|
90
|
+
`node_modules/` vb.) otomatik olarak dışarıda kalır.
|
|
91
|
+
Git reposu yoksa (henüz `git init` edilmemiş dizin) eski davranışa
|
|
92
|
+
devam edilir — dizin bazlı filtreleme uygulanır.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
repo_path: Repo kök dizini (None ise mevcut dizin)
|
|
96
|
+
max_files: Maksimum taranacak dosya sayısı
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
FileAnalysisResult listesi
|
|
100
|
+
"""
|
|
101
|
+
kok = Path(repo_path or Path.cwd()).resolve()
|
|
102
|
+
repo = get_repo(kok)
|
|
103
|
+
|
|
104
|
+
desteklenen_uzantilar = {".py", ".js", ".jsx", ".ts", ".tsx"}
|
|
105
|
+
|
|
106
|
+
# Dizin bazlı kara liste — git olmayan repolar + ekstra güvenlik katmanı
|
|
107
|
+
atlanan_dizinler = {
|
|
108
|
+
".git", "__pycache__", "node_modules", ".venv", "venv",
|
|
109
|
+
"dist", "build", ".mypy_cache", ".pytest_cache",
|
|
110
|
+
".next", # Next.js build çıktısı
|
|
111
|
+
"out", # Next.js static export / tsc çıktısı
|
|
112
|
+
".turbo", # Turborepo önbelleği
|
|
113
|
+
".parcel-cache",
|
|
114
|
+
"coverage",
|
|
115
|
+
".nyc_output",
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
# Git'in izlediği dosyalar — bu liste asıl filtre olarak kullanılır
|
|
119
|
+
izlenen_dosyalar: set[str] = set()
|
|
120
|
+
if repo:
|
|
121
|
+
try:
|
|
122
|
+
izlenen_dosyalar = {item for item in repo.git.ls_files().splitlines()}
|
|
123
|
+
except Exception:
|
|
124
|
+
pass
|
|
125
|
+
|
|
126
|
+
sonuclar: list[FileAnalysisResult] = []
|
|
127
|
+
sayac = 0
|
|
128
|
+
|
|
129
|
+
for dosya in kok.rglob("*"):
|
|
130
|
+
if sayac >= max_files:
|
|
131
|
+
break
|
|
132
|
+
|
|
133
|
+
# Kara listedeki dizinleri geç (git olmayan repolar için de çalışır)
|
|
134
|
+
parcalar = dosya.parts
|
|
135
|
+
if any(atla in parcalar for atla in atlanan_dizinler):
|
|
136
|
+
continue
|
|
137
|
+
|
|
138
|
+
if not dosya.is_file():
|
|
139
|
+
continue
|
|
140
|
+
|
|
141
|
+
if dosya.suffix.lower() not in desteklenen_uzantilar:
|
|
142
|
+
continue
|
|
143
|
+
|
|
144
|
+
goreceli_yol = str(dosya.relative_to(kok))
|
|
145
|
+
|
|
146
|
+
# Asıl fix: Git reposu varsa SADECE git'in takip ettiği dosyaları tara.
|
|
147
|
+
# Bu, .gitignore'daki tüm build/üretilen dosyaları otomatik dışlar.
|
|
148
|
+
# Git yoksa bu filtreyi atla — dizin kara listesi yeterli koruma sağlar.
|
|
149
|
+
if repo and izlenen_dosyalar and goreceli_yol not in izlenen_dosyalar:
|
|
150
|
+
continue
|
|
151
|
+
|
|
152
|
+
# single_commit_ratio hesapla (git izliyorsa)
|
|
153
|
+
tek_commit_orani = 0.0
|
|
154
|
+
if repo and goreceli_yol in izlenen_dosyalar:
|
|
155
|
+
tek_commit_orani = calculate_single_commit_ratio(repo, goreceli_yol)
|
|
156
|
+
|
|
157
|
+
sonuc = analyze_file(dosya, single_commit_ratio=tek_commit_orani)
|
|
158
|
+
if not sonuc.desteklenmiyor and sonuc.hata is None:
|
|
159
|
+
sonuclar.append(sonuc)
|
|
160
|
+
sayac += 1
|
|
161
|
+
|
|
162
|
+
return sonuclar
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def get_commit_files(repo: Repo, commit_hash: Optional[str] = None) -> list[str]:
|
|
166
|
+
"""
|
|
167
|
+
Belirli bir commit'teki (ya da HEAD'deki) değişen dosyaları listele.
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
repo: Git repo nesnesi
|
|
171
|
+
commit_hash: İncelenecek commit hash'i (None ise HEAD)
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
Değişen dosyaların yol listesi
|
|
175
|
+
"""
|
|
176
|
+
try:
|
|
177
|
+
if commit_hash:
|
|
178
|
+
commit = repo.commit(commit_hash)
|
|
179
|
+
else:
|
|
180
|
+
commit = repo.head.commit
|
|
181
|
+
|
|
182
|
+
return list(commit.stats.files.keys())
|
|
183
|
+
except Exception:
|
|
184
|
+
return []
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def score_latest_commit(repo_path: Optional[Path] = None) -> tuple[Optional[str], list[FileAnalysisResult]]:
|
|
188
|
+
"""
|
|
189
|
+
Son commit'teki dosyaları analiz et.
|
|
190
|
+
|
|
191
|
+
Returns:
|
|
192
|
+
(commit_hash, sonuç_listesi) çifti
|
|
193
|
+
"""
|
|
194
|
+
kok = Path(repo_path or Path.cwd()).resolve()
|
|
195
|
+
repo = get_repo(kok)
|
|
196
|
+
if not repo:
|
|
197
|
+
return None, []
|
|
198
|
+
|
|
199
|
+
try:
|
|
200
|
+
commit = repo.head.commit
|
|
201
|
+
commit_hash = commit.hexsha
|
|
202
|
+
dosyalar = get_commit_files(repo, commit_hash)
|
|
203
|
+
except Exception:
|
|
204
|
+
return None, []
|
|
205
|
+
|
|
206
|
+
sonuclar: list[FileAnalysisResult] = []
|
|
207
|
+
desteklenen_uzantilar = {".py", ".js", ".jsx", ".ts", ".tsx"}
|
|
208
|
+
|
|
209
|
+
for dosya_yolu in dosyalar:
|
|
210
|
+
tam_yol = kok / dosya_yolu
|
|
211
|
+
if not tam_yol.exists():
|
|
212
|
+
continue
|
|
213
|
+
if tam_yol.suffix.lower() not in desteklenen_uzantilar:
|
|
214
|
+
continue
|
|
215
|
+
|
|
216
|
+
tek_commit_orani = calculate_single_commit_ratio(repo, dosya_yolu)
|
|
217
|
+
sonuc = analyze_file(tam_yol, single_commit_ratio=tek_commit_orani)
|
|
218
|
+
if not sonuc.desteklenmiyor and sonuc.hata is None:
|
|
219
|
+
sonuclar.append(sonuc)
|
|
220
|
+
|
|
221
|
+
return commit_hash, sonuclar
|
codedna/sprint_health.py
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"""Sprint bazlı kod sağlığı skoru hesaplama."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
from codedna.db import get_connection, get_sprint_history, save_sprint
|
|
11
|
+
|
|
12
|
+
# Sağlık skoru eşikleri (0-100)
|
|
13
|
+
_SAGLIKLI_ESIK = 80
|
|
14
|
+
_DIKKAT_ESIK = 50
|
|
15
|
+
|
|
16
|
+
# Yüksek riskli AI eşiği
|
|
17
|
+
_YUKSEK_RISK_AI = 0.7
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class SprintSonucu:
|
|
22
|
+
"""Tek sprint'in sağlık analizi."""
|
|
23
|
+
|
|
24
|
+
sprint_adi: str
|
|
25
|
+
baslangic: datetime
|
|
26
|
+
bitis: datetime
|
|
27
|
+
health_score: float # 0-100
|
|
28
|
+
durum: str # SAĞLIKLI / DİKKAT / RİSKLİ
|
|
29
|
+
avg_understanding: Optional[float]
|
|
30
|
+
ai_orani: float # yüksek riskli AI dosyalarının oranı
|
|
31
|
+
debt_delta_saati: float # borç değişimi (+ = arttı, - = azaldı)
|
|
32
|
+
toplam_commit: int
|
|
33
|
+
ai_satir: int
|
|
34
|
+
insan_satir: int
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def ai_insan_orani_str(self) -> str:
|
|
38
|
+
"""AI/insan oranını yüzde string olarak döndür."""
|
|
39
|
+
toplam = self.ai_satir + self.insan_satir
|
|
40
|
+
if toplam == 0:
|
|
41
|
+
return "N/A"
|
|
42
|
+
ai_pct = self.ai_satir / toplam * 100
|
|
43
|
+
return f"%{ai_pct:.0f} AI / %{100-ai_pct:.0f} İnsan"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _durum_belirle(skor: float) -> str:
|
|
47
|
+
"""Skor aralığına göre durum etiketi döndür."""
|
|
48
|
+
if skor >= _SAGLIKLI_ESIK:
|
|
49
|
+
return "SAĞLIKLI"
|
|
50
|
+
elif skor >= _DIKKAT_ESIK:
|
|
51
|
+
return "DİKKAT"
|
|
52
|
+
return "RİSKLİ"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def calculate_sprint_health(
|
|
56
|
+
repo_path: Path,
|
|
57
|
+
db_path: Path,
|
|
58
|
+
start_date: datetime,
|
|
59
|
+
end_date: datetime,
|
|
60
|
+
sprint_adi: str = "Sprint",
|
|
61
|
+
) -> SprintSonucu:
|
|
62
|
+
"""
|
|
63
|
+
Verilen tarih aralığındaki commitleri analiz edip sprint sağlık skoru üret.
|
|
64
|
+
|
|
65
|
+
Health score formülü (0-100):
|
|
66
|
+
- anlama_puani = (avg_understanding / 5) * 40 max 40 puan
|
|
67
|
+
- ai_dengesi = (1 - yüksek_riskli_ai_orani) * 30 max 30 puan
|
|
68
|
+
- borc_trendi = max(0, 1 - delta_oran) * 30 max 30 puan
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
repo_path: Git repo kök dizini
|
|
72
|
+
db_path: SQLite veritabanı yolu
|
|
73
|
+
start_date: Sprint başlangıç tarihi
|
|
74
|
+
end_date: Sprint bitiş tarihi
|
|
75
|
+
sprint_adi: Sprint ismi
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
SprintSonucu nesnesi
|
|
79
|
+
"""
|
|
80
|
+
start_ts = int(start_date.timestamp())
|
|
81
|
+
end_ts = int(end_date.timestamp())
|
|
82
|
+
|
|
83
|
+
# Sprint tarih aralığındaki commit'leri ve dosya skorlarını çek
|
|
84
|
+
try:
|
|
85
|
+
with get_connection(db_path) as conn:
|
|
86
|
+
commitler = conn.execute(
|
|
87
|
+
"""
|
|
88
|
+
SELECT c.commit_hash, c.understanding_score, c.timestamp
|
|
89
|
+
FROM commits c
|
|
90
|
+
WHERE c.timestamp >= ? AND c.timestamp <= ?
|
|
91
|
+
ORDER BY c.timestamp ASC
|
|
92
|
+
""",
|
|
93
|
+
(start_ts, end_ts),
|
|
94
|
+
).fetchall()
|
|
95
|
+
except Exception:
|
|
96
|
+
commitler = []
|
|
97
|
+
|
|
98
|
+
toplam_commit = len(commitler)
|
|
99
|
+
|
|
100
|
+
# Dosya skorlarını topla
|
|
101
|
+
ai_skorlari: list[float] = []
|
|
102
|
+
anlama_skorlari: list[float] = []
|
|
103
|
+
|
|
104
|
+
try:
|
|
105
|
+
with get_connection(db_path) as conn:
|
|
106
|
+
for commit in commitler:
|
|
107
|
+
dosyalar = conn.execute(
|
|
108
|
+
"""
|
|
109
|
+
SELECT ai_probability, understanding_score
|
|
110
|
+
FROM file_scores
|
|
111
|
+
WHERE commit_hash = ?
|
|
112
|
+
""",
|
|
113
|
+
(commit["commit_hash"],),
|
|
114
|
+
).fetchall()
|
|
115
|
+
for d in dosyalar:
|
|
116
|
+
if d["ai_probability"] is not None:
|
|
117
|
+
ai_skorlari.append(float(d["ai_probability"]))
|
|
118
|
+
if d["understanding_score"] is not None:
|
|
119
|
+
anlama_skorlari.append(float(d["understanding_score"]))
|
|
120
|
+
except Exception:
|
|
121
|
+
pass
|
|
122
|
+
|
|
123
|
+
# Ortalama anlama skoru
|
|
124
|
+
avg_anlama = (
|
|
125
|
+
sum(anlama_skorlari) / len(anlama_skorlari)
|
|
126
|
+
if anlama_skorlari else None
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
# Yüksek riskli AI oranı (>= 0.7)
|
|
130
|
+
yuksek_riskli = sum(1 for a in ai_skorlari if a >= _YUKSEK_RISK_AI)
|
|
131
|
+
ai_orani = yuksek_riskli / len(ai_skorlari) if ai_skorlari else 0.0
|
|
132
|
+
|
|
133
|
+
# AI / insan satır tahmini (AI skoru > 0.5 → AI satır sayılır)
|
|
134
|
+
ai_satir = sum(1 for a in ai_skorlari if a > 0.5) * 50 # yaklaşık
|
|
135
|
+
insan_satir = max(len(ai_skorlari) * 50 - ai_satir, 0)
|
|
136
|
+
|
|
137
|
+
# Teknik borç delta'sı — tüm repo borcu hesapla (sprint başı/sonu farkı yok,
|
|
138
|
+
# mevcut durumu baz al, negatif = borç azaldı yorumu)
|
|
139
|
+
from codedna.tech_debt import calculate_repo_debt
|
|
140
|
+
try:
|
|
141
|
+
ozet = calculate_repo_debt(repo_path, db_path)
|
|
142
|
+
debt_delta = ozet.toplam_debt_saatleri / max(toplam_commit, 1)
|
|
143
|
+
except Exception:
|
|
144
|
+
debt_delta = 0.0
|
|
145
|
+
|
|
146
|
+
# ---- Health score hesapla ----
|
|
147
|
+
# 1. Anlama puanı (max 40)
|
|
148
|
+
anlama_puani = ((avg_anlama / 5.0) * 40.0) if avg_anlama is not None else 20.0
|
|
149
|
+
|
|
150
|
+
# 2. AI denge puanı (max 30)
|
|
151
|
+
ai_dengesi = (1.0 - ai_orani) * 30.0
|
|
152
|
+
|
|
153
|
+
# 3. Borç trendi puanı (max 30) — debt_delta küçükse iyi
|
|
154
|
+
delta_oran = min(debt_delta / 10.0, 1.0) # 10 saate normalize
|
|
155
|
+
borc_trendi = max(0.0, 1.0 - delta_oran) * 30.0
|
|
156
|
+
|
|
157
|
+
health_score = round(anlama_puani + ai_dengesi + borc_trendi, 1)
|
|
158
|
+
durum = _durum_belirle(health_score)
|
|
159
|
+
|
|
160
|
+
return SprintSonucu(
|
|
161
|
+
sprint_adi=sprint_adi,
|
|
162
|
+
baslangic=start_date,
|
|
163
|
+
bitis=end_date,
|
|
164
|
+
health_score=health_score,
|
|
165
|
+
durum=durum,
|
|
166
|
+
avg_understanding=round(avg_anlama, 2) if avg_anlama is not None else None,
|
|
167
|
+
ai_orani=round(ai_orani, 3),
|
|
168
|
+
debt_delta_saati=round(debt_delta, 2),
|
|
169
|
+
toplam_commit=toplam_commit,
|
|
170
|
+
ai_satir=ai_satir,
|
|
171
|
+
insan_satir=insan_satir,
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def save_sprint_result(sonuc: SprintSonucu, db_path: Path) -> int:
|
|
176
|
+
"""Sprint sonucunu DB'ye kaydet, yeni id döndür."""
|
|
177
|
+
return save_sprint(
|
|
178
|
+
sprint_name=sonuc.sprint_adi,
|
|
179
|
+
start_date=int(sonuc.baslangic.timestamp()),
|
|
180
|
+
end_date=int(sonuc.bitis.timestamp()),
|
|
181
|
+
total_lines_ai=sonuc.ai_satir,
|
|
182
|
+
total_lines_human=sonuc.insan_satir,
|
|
183
|
+
avg_understanding=sonuc.avg_understanding,
|
|
184
|
+
debt_delta_hours=sonuc.debt_delta_saati,
|
|
185
|
+
health_score=sonuc.health_score,
|
|
186
|
+
db_path=db_path,
|
|
187
|
+
)
|