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/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
@@ -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
+ )