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/auth.py ADDED
@@ -0,0 +1,372 @@
1
+ """Kullanıcı kimlik doğrulama — kayıt, giriş, JWT oturum yönetimi."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import os
7
+ import re
8
+ import time
9
+ from pathlib import Path
10
+ from typing import Optional
11
+
12
+ import bcrypt
13
+ import jwt
14
+
15
+ from codedna.db import get_connection
16
+
17
+ # ---------------------------------------------------------------------------
18
+ # Yapılandırma
19
+ # ---------------------------------------------------------------------------
20
+
21
+ # JWT secret MUTLAKA ortam değişkeninden okunmalı — koda gömülmemeli
22
+ JWT_SECRET: Optional[str] = os.environ.get("CODEDNA_JWT_SECRET")
23
+ JWT_ALGORITHM = "HS256"
24
+ JWT_EXPIRY_HOURS = 24 * 7 # 7 gün
25
+
26
+ # Auth veritabanı yolu — repo db'sinden ayrı
27
+ _AUTH_DB_VARSAYILAN = Path.home() / ".codedna" / "auth.db"
28
+
29
+
30
+ def _auth_db() -> Path:
31
+ """Auth veritabanı yolunu döndür (ortam değişkeninden veya varsayılan)."""
32
+ env = os.environ.get("CODEDNA_AUTH_DB_PATH")
33
+ return Path(env).resolve() if env else _AUTH_DB_VARSAYILAN
34
+
35
+
36
+ def _jwt_secret_kontrol() -> str:
37
+ """
38
+ JWT secret'ın var olduğunu doğrula.
39
+
40
+ Production'da secret olmadan uygulama başlamamalı.
41
+ Development/test için otomatik rastgele secret üretir (persistent değil).
42
+ """
43
+ secret = JWT_SECRET or os.environ.get("CODEDNA_JWT_SECRET")
44
+ if not secret:
45
+ # Development modunda otomatik dev secret üret (process başına)
46
+ # Production'da CODEDNA_JWT_SECRET environment variable tanımlanmalı
47
+ secret = os.environ.get("CODEDNA_JWT_DEV_SECRET")
48
+ if not secret:
49
+ import secrets
50
+ secret = "dev-" + secrets.token_urlsafe(48)
51
+ os.environ["CODEDNA_JWT_DEV_SECRET"] = secret
52
+ import warnings
53
+ warnings.warn(
54
+ "CODEDNA_JWT_SECRET tanımlı değil — development secret kullanılıyor. "
55
+ "Production için CODEDNA_JWT_SECRET environment variable tanımlayın.",
56
+ RuntimeWarning,
57
+ stacklevel=2
58
+ )
59
+ return secret
60
+
61
+
62
+ # ---------------------------------------------------------------------------
63
+ # Auth DB şeması
64
+ # ---------------------------------------------------------------------------
65
+
66
+ def init_auth_db(db_path: Optional[Path] = None) -> None:
67
+ """Kullanıcı ve oturum tablolarını oluştur (yoksa)."""
68
+ yol = db_path or _auth_db()
69
+ yol.parent.mkdir(parents=True, exist_ok=True)
70
+ with get_connection(yol) as conn:
71
+ conn.executescript("""
72
+ CREATE TABLE IF NOT EXISTS users (
73
+ id INTEGER PRIMARY KEY,
74
+ email TEXT UNIQUE NOT NULL,
75
+ password_hash TEXT NOT NULL,
76
+ plan TEXT DEFAULT 'free',
77
+ lemonsqueezy_customer_id TEXT,
78
+ lemonsqueezy_subscription_id TEXT,
79
+ subscription_status TEXT DEFAULT 'none',
80
+ created_at INTEGER,
81
+ updated_at INTEGER
82
+ );
83
+
84
+ CREATE TABLE IF NOT EXISTS sessions (
85
+ id INTEGER PRIMARY KEY,
86
+ user_id INTEGER NOT NULL,
87
+ token_hash TEXT UNIQUE NOT NULL,
88
+ created_at INTEGER,
89
+ expires_at INTEGER,
90
+ FOREIGN KEY (user_id) REFERENCES users(id)
91
+ );
92
+ """)
93
+
94
+
95
+ # ---------------------------------------------------------------------------
96
+ # Şifre işlemleri
97
+ # ---------------------------------------------------------------------------
98
+
99
+ def hash_password(password: str) -> str:
100
+ """bcrypt ile şifreyi hash'le."""
101
+ return bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt()).decode("utf-8")
102
+
103
+
104
+ def verify_password(password: str, password_hash: str) -> bool:
105
+ """Şifreyi hash ile güvenli biçimde karşılaştır."""
106
+ try:
107
+ return bcrypt.checkpw(password.encode("utf-8"), password_hash.encode("utf-8"))
108
+ except Exception:
109
+ return False
110
+
111
+
112
+ # ---------------------------------------------------------------------------
113
+ # Token yardımcıları
114
+ # ---------------------------------------------------------------------------
115
+
116
+ def _token_hash(token: str) -> str:
117
+ """Token'ı SHA-256 ile hash'le — DB'de token'ın kendisi SAKLANMAZ."""
118
+ return hashlib.sha256(token.encode("utf-8")).hexdigest()
119
+
120
+
121
+ def _jwt_olustur(user_id: int, email: str, plan: str) -> str:
122
+ """JWT token üret."""
123
+ secret = _jwt_secret_kontrol()
124
+ su_an = int(time.time())
125
+ payload = {
126
+ "sub": str(user_id),
127
+ "email": email,
128
+ "plan": plan,
129
+ "iat": su_an,
130
+ "exp": su_an + JWT_EXPIRY_HOURS * 3600,
131
+ }
132
+ return jwt.encode(payload, secret, algorithm=JWT_ALGORITHM)
133
+
134
+
135
+ def _jwt_coz(token: str) -> Optional[dict]:
136
+ """JWT token'ı çöz, geçersizse None döndür."""
137
+ secret = _jwt_secret_kontrol()
138
+ try:
139
+ return jwt.decode(token, secret, algorithms=[JWT_ALGORITHM])
140
+ except jwt.ExpiredSignatureError:
141
+ return None
142
+ except jwt.InvalidTokenError:
143
+ return None
144
+
145
+
146
+ # ---------------------------------------------------------------------------
147
+ # Kayıt / Giriş / Çıkış
148
+ # ---------------------------------------------------------------------------
149
+
150
+ _EMAIL_REGEX = re.compile(r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$")
151
+
152
+
153
+ def register_user(
154
+ email: str,
155
+ password: str,
156
+ db_path: Optional[Path] = None,
157
+ ) -> dict:
158
+ """
159
+ Yeni kullanıcı kaydı yap.
160
+
161
+ Kurallar:
162
+ - E-posta formatı geçerli olmalı
163
+ - Şifre en az 8 karakter
164
+ - E-posta zaten kayıtlıysa hata
165
+
166
+ Args:
167
+ email: Kullanıcı e-postası
168
+ password: Düz metin şifre (kayıt sonrası saklanmaz)
169
+ db_path: Auth DB yolu
170
+
171
+ Returns:
172
+ {"user_id", "token", "plan"} sözlüğü
173
+ """
174
+ yol = db_path or _auth_db()
175
+ init_auth_db(yol)
176
+
177
+ # Girdi doğrulama
178
+ email = email.strip().lower()
179
+ if not _EMAIL_REGEX.match(email):
180
+ raise ValueError("Geçersiz e-posta formatı.")
181
+ if len(password) < 8:
182
+ raise ValueError("Şifre en az 8 karakter olmalı.")
183
+
184
+ sifre_hash = hash_password(password)
185
+ su_an = int(time.time())
186
+
187
+ # Lisans dosyasından plan al (demo/single-tenant modu)
188
+ # Production'da plan satın alma üzerinden atanır
189
+ try:
190
+ from codedna.plan import get_current_plan
191
+ initial_plan = get_current_plan().value
192
+ except Exception:
193
+ initial_plan = "free"
194
+
195
+ try:
196
+ with get_connection(yol) as conn:
197
+ cur = conn.execute(
198
+ """
199
+ INSERT INTO users (email, password_hash, plan, subscription_status, created_at, updated_at)
200
+ VALUES (?, ?, ?, 'demo', ?, ?)
201
+ """,
202
+ (email, sifre_hash, initial_plan, su_an, su_an),
203
+ )
204
+ user_id = cur.lastrowid or 0
205
+ except Exception as e:
206
+ if "UNIQUE" in str(e).upper():
207
+ raise ValueError("Bu e-posta adresi zaten kayıtlı.")
208
+ raise
209
+
210
+ token = _jwt_olustur(user_id, email, initial_plan)
211
+ _oturum_kaydet(user_id, token, yol)
212
+
213
+ return {"user_id": user_id, "token": token, "plan": initial_plan}
214
+
215
+
216
+ def login_user(
217
+ email: str,
218
+ password: str,
219
+ db_path: Optional[Path] = None,
220
+ ) -> dict:
221
+ """
222
+ Giriş yap ve JWT token üret.
223
+
224
+ Güvenlik notu: Başarısız girişlerde spesifik hata döndürme
225
+ (kullanıcı enumeration saldırısını önlemek için genel mesaj kullan).
226
+
227
+ Args:
228
+ email: Kullanıcı e-postası
229
+ password: Düz metin şifre
230
+
231
+ Returns:
232
+ {"user_id", "token", "plan", "subscription_status"} sözlüğü
233
+ """
234
+ yol = db_path or _auth_db()
235
+ init_auth_db(yol)
236
+
237
+ email = email.strip().lower()
238
+
239
+ with get_connection(yol) as conn:
240
+ kullanici = conn.execute(
241
+ "SELECT id, password_hash, plan, subscription_status FROM users WHERE email = ?",
242
+ (email,),
243
+ ).fetchone()
244
+
245
+ # Kullanıcı yoksa veya şifre yanlışsa AYNI hata — enumeration önlemi
246
+ if not kullanici or not verify_password(password, kullanici["password_hash"]):
247
+ raise ValueError("E-posta veya şifre hatalı.")
248
+
249
+ token = _jwt_olustur(kullanici["id"], email, kullanici["plan"])
250
+ _oturum_kaydet(kullanici["id"], token, yol)
251
+
252
+ return {
253
+ "user_id": kullanici["id"],
254
+ "token": token,
255
+ "plan": kullanici["plan"],
256
+ "subscription_status": kullanici["subscription_status"],
257
+ }
258
+
259
+
260
+ def verify_token(
261
+ token: str,
262
+ db_path: Optional[Path] = None,
263
+ ) -> Optional[dict]:
264
+ """
265
+ JWT token'ı doğrula ve kullanıcı bilgisini döndür.
266
+
267
+ Hem JWT imzasını hem de oturum tablosundaki kaydı kontrol eder.
268
+
269
+ Returns:
270
+ Geçerliyse {"user_id", "email", "plan"}, değilse None
271
+ """
272
+ yol = db_path or _auth_db()
273
+ payload = _jwt_coz(token)
274
+ if not payload:
275
+ return None
276
+
277
+ token_h = _token_hash(token)
278
+ su_an = int(time.time())
279
+
280
+ with get_connection(yol) as conn:
281
+ oturum = conn.execute(
282
+ "SELECT id FROM sessions WHERE token_hash = ? AND expires_at > ?",
283
+ (token_h, su_an),
284
+ ).fetchone()
285
+
286
+ if not oturum:
287
+ return None
288
+
289
+ return {
290
+ "user_id": int(payload["sub"]),
291
+ "email": payload["email"],
292
+ "plan": payload["plan"],
293
+ }
294
+
295
+
296
+ def logout_user(
297
+ token: str,
298
+ db_path: Optional[Path] = None,
299
+ ) -> bool:
300
+ """
301
+ Oturumu sonlandır — sessions tablosundan sil.
302
+
303
+ Returns:
304
+ Başarıyla silinirse True
305
+ """
306
+ yol = db_path or _auth_db()
307
+ token_h = _token_hash(token)
308
+ with get_connection(yol) as conn:
309
+ cur = conn.execute("DELETE FROM sessions WHERE token_hash = ?", (token_h,))
310
+ return cur.rowcount > 0
311
+
312
+
313
+ def get_user_by_id(user_id: int, db_path: Optional[Path] = None) -> Optional[dict]:
314
+ """Kullanıcı bilgisini ID ile getir (şifre hash hariç)."""
315
+ yol = db_path or _auth_db()
316
+ with get_connection(yol) as conn:
317
+ row = conn.execute(
318
+ "SELECT id, email, plan, subscription_status, lemonsqueezy_customer_id FROM users WHERE id = ?",
319
+ (user_id,),
320
+ ).fetchone()
321
+ if not row:
322
+ return None
323
+ return {
324
+ "id": row["id"],
325
+ "email": row["email"],
326
+ "plan": row["plan"],
327
+ "subscription_status": row["subscription_status"],
328
+ "lemonsqueezy_customer_id": row["lemonsqueezy_customer_id"],
329
+ }
330
+
331
+
332
+ def update_user_plan(
333
+ user_id: int,
334
+ plan: str,
335
+ subscription_status: str,
336
+ customer_id: Optional[str] = None,
337
+ subscription_id: Optional[str] = None,
338
+ db_path: Optional[Path] = None,
339
+ ) -> None:
340
+ """Kullanıcının plan ve abonelik durumunu güncelle."""
341
+ yol = db_path or _auth_db()
342
+ su_an = int(time.time())
343
+ with get_connection(yol) as conn:
344
+ conn.execute(
345
+ """
346
+ UPDATE users
347
+ SET plan = ?, subscription_status = ?,
348
+ lemonsqueezy_customer_id = COALESCE(?, lemonsqueezy_customer_id),
349
+ lemonsqueezy_subscription_id = COALESCE(?, lemonsqueezy_subscription_id),
350
+ updated_at = ?
351
+ WHERE id = ?
352
+ """,
353
+ (plan, subscription_status, customer_id, subscription_id, su_an, user_id),
354
+ )
355
+
356
+
357
+ # ---------------------------------------------------------------------------
358
+ # Yardımcı — oturum kaydetme
359
+ # ---------------------------------------------------------------------------
360
+
361
+ def _oturum_kaydet(user_id: int, token: str, db_path: Path) -> None:
362
+ """Yeni oturumu DB'ye kaydet (token'ın hash'i saklanır)."""
363
+ su_an = int(time.time())
364
+ bitis = su_an + JWT_EXPIRY_HOURS * 3600
365
+ token_h = _token_hash(token)
366
+ with get_connection(db_path) as conn:
367
+ # Eski süresi dolmuş oturumları temizle
368
+ conn.execute("DELETE FROM sessions WHERE expires_at < ?", (su_an,))
369
+ conn.execute(
370
+ "INSERT OR IGNORE INTO sessions (user_id, token_hash, created_at, expires_at) VALUES (?, ?, ?, ?)",
371
+ (user_id, token_h, su_an, bitis),
372
+ )
codedna/bus_factor.py ADDED
@@ -0,0 +1,259 @@
1
+ """Bus factor hesaplama — bir dosyayı kaç kişi gerçekten anlıyor."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import subprocess
6
+ from collections import defaultdict
7
+ from dataclasses import dataclass, field
8
+ from pathlib import Path
9
+ from typing import Optional
10
+
11
+ from codedna.db import get_file_ownership, upsert_file_ownership
12
+
13
+ # Büyük repo uyarı eşiği
14
+ _BUYUK_REPO_ESIGI = 500
15
+
16
+ # Yeterli anlayış eşiği (bu skorun altındaki yazarlar "anlıyor" sayılmaz)
17
+ _ANLAMA_ESIGI = 3.5
18
+
19
+ # Birincil sahip sayılmak için gereken minimum satır yüzdesi
20
+ _SAHIPLIK_ESIGI = 0.10 # %10 → o dosyada "ilgili" sayılır
21
+
22
+
23
+ @dataclass
24
+ class DosyaSahiplik:
25
+ """Tek bir dosyanın yazar bazlı sahiplik özeti."""
26
+
27
+ dosya_yolu: str
28
+ toplam_satir: int
29
+ yazarlar: dict[str, int] = field(default_factory=dict) # yazar → satır sayısı
30
+
31
+ @property
32
+ def birincil_sahip(self) -> Optional[str]:
33
+ """En fazla satıra sahip yazarı döndür."""
34
+ if not self.yazarlar:
35
+ return None
36
+ return max(self.yazarlar, key=lambda y: self.yazarlar[y])
37
+
38
+ @property
39
+ def birincil_sahiplik_yuzdesi(self) -> float:
40
+ """Birincil sahibin sahiplik yüzdesi (0.0–1.0)."""
41
+ if not self.yazarlar or self.toplam_satir == 0:
42
+ return 0.0
43
+ birincil = self.birincil_sahip
44
+ return self.yazarlar.get(birincil or "", 0) / self.toplam_satir
45
+
46
+
47
+ @dataclass
48
+ class BusFaktorSonucu:
49
+ """Tek dosya için bus factor sonucu."""
50
+
51
+ dosya_yolu: str
52
+ bus_factor: int # kaç kişi bu dosyayı anlıyor
53
+ birincil_sahip: Optional[str]
54
+ sahiplik_yuzdesi: float # birincil sahibin yüzdesi
55
+ risk: str # KRİTİK / RİSKLİ / GÜVENLİ
56
+ anlayan_yazarlar: list[str] # anlama skoru >= eşik olan yazarlar
57
+ toplam_satir: int
58
+
59
+
60
+ def _git_blame_calistir(dosya_yolu: Path, repo_koku: Path) -> dict[str, int]:
61
+ """
62
+ git blame --line-porcelain ile satır bazlı yazar tespiti yap.
63
+
64
+ Returns:
65
+ {yazar_adi: satir_sayisi} sözlüğü
66
+ """
67
+ try:
68
+ goreceli = dosya_yolu.relative_to(repo_koku)
69
+ except ValueError:
70
+ goreceli = dosya_yolu
71
+
72
+ try:
73
+ sonuc = subprocess.run(
74
+ ["git", "blame", "--line-porcelain", str(goreceli)],
75
+ cwd=str(repo_koku),
76
+ capture_output=True,
77
+ text=True,
78
+ timeout=30,
79
+ encoding="utf-8",
80
+ errors="replace",
81
+ )
82
+ except (subprocess.TimeoutExpired, FileNotFoundError):
83
+ return {}
84
+
85
+ if sonuc.returncode != 0:
86
+ return {}
87
+
88
+ # Çıktıdan "author " satırlarını topla
89
+ yazar_satirlari: dict[str, int] = defaultdict(int)
90
+ for satir in sonuc.stdout.splitlines():
91
+ if satir.startswith("author "):
92
+ yazar = satir[7:].strip()
93
+ # Özel "Not Committed Yet" durumunu atla
94
+ if yazar and yazar != "Not Committed Yet":
95
+ yazar_satirlari[yazar] += 1
96
+
97
+ return dict(yazar_satirlari)
98
+
99
+
100
+ def _dosya_listesi_al(repo_koku: Path, max_dosya: int = _BUYUK_REPO_ESIGI) -> list[Path]:
101
+ """
102
+ Git tarafından izlenen desteklenen kaynak dosyaları listele.
103
+
104
+ Args:
105
+ repo_koku: Git repo kök dizini
106
+ max_dosya: İşlenecek maksimum dosya sayısı (performans için)
107
+
108
+ Returns:
109
+ Mutlak Path listesi
110
+ """
111
+ desteklenen = {".py", ".js", ".jsx", ".ts", ".tsx"}
112
+
113
+ try:
114
+ sonuc = subprocess.run(
115
+ ["git", "ls-files"],
116
+ cwd=str(repo_koku),
117
+ capture_output=True,
118
+ text=True,
119
+ timeout=10,
120
+ encoding="utf-8",
121
+ errors="replace",
122
+ )
123
+ dosyalar = sonuc.stdout.strip().splitlines()
124
+ except Exception:
125
+ dosyalar = []
126
+
127
+ filtreli = [
128
+ repo_koku / d for d in dosyalar
129
+ if Path(d).suffix.lower() in desteklenen and (repo_koku / d).exists()
130
+ ]
131
+ return filtreli[:max_dosya]
132
+
133
+
134
+ def calculate_bus_factor(
135
+ repo_path: Path,
136
+ db_path: Path,
137
+ max_dosya: int = _BUYUK_REPO_ESIGI,
138
+ ) -> list[BusFaktorSonucu]:
139
+ """
140
+ Repo genelinde her dosya için bus factor hesapla ve DB'ye kaydet.
141
+
142
+ Bus factor = o dosyanın %50'sinden fazlasına sahip OLAN
143
+ VE understanding_score >= 3.5 olan yazar sayısı.
144
+ Anlama skoru yoksa sahiplik oranı >= 10% yeterli sayılır.
145
+
146
+ Args:
147
+ repo_path: Git repo kök dizini
148
+ max_dosya: İşlenecek maksimum dosya sayısı
149
+
150
+ Returns:
151
+ BusFaktorSonucu listesi, bus_factor'a göre artan sırada
152
+ """
153
+ kok = Path(repo_path).resolve()
154
+ dosyalar = _dosya_listesi_al(kok, max_dosya)
155
+
156
+ # Mevcut anlama skorlarını DB'den tek sorguda çek
157
+ from codedna.db import get_connection
158
+ anlama_map: dict[tuple[str, str], float] = {} # (dosya_yolu, yazar) → skor
159
+ try:
160
+ with get_connection(db_path) as conn:
161
+ rows = conn.execute(
162
+ """
163
+ SELECT fo.file_path, fo.author, fo.avg_understanding
164
+ FROM file_ownership fo
165
+ WHERE fo.avg_understanding IS NOT NULL
166
+ """
167
+ ).fetchall()
168
+ for r in rows:
169
+ anlama_map[(r["file_path"], r["author"])] = float(r["avg_understanding"])
170
+ except Exception:
171
+ pass
172
+
173
+ sonuclar: list[BusFaktorSonucu] = []
174
+
175
+ for dosya in dosyalar:
176
+ yazar_satirlari = _git_blame_calistir(dosya, kok)
177
+ if not yazar_satirlari:
178
+ continue
179
+
180
+ toplam = sum(yazar_satirlari.values())
181
+ if toplam == 0:
182
+ continue
183
+
184
+ # DB'ye kaydet
185
+ for yazar, satir in yazar_satirlari.items():
186
+ anlama = anlama_map.get((str(dosya), yazar))
187
+ upsert_file_ownership(
188
+ file_path=str(dosya),
189
+ author=yazar,
190
+ lines_owned=satir,
191
+ last_touched=0, # gelecekte git log ile doldurulacak
192
+ avg_understanding=anlama,
193
+ db_path=db_path,
194
+ )
195
+
196
+ # "Anlayan" yazarları belirle
197
+ anlayan: list[str] = []
198
+ for yazar, satir in yazar_satirlari.items():
199
+ yuzde = satir / toplam
200
+ # Anlama skoru varsa kontrol et, yoksa %10+ sahiplik yeterli
201
+ anlama_skoru = anlama_map.get((str(dosya), yazar))
202
+ if anlama_skoru is not None:
203
+ if anlama_skoru >= _ANLAMA_ESIGI and yuzde >= _SAHIPLIK_ESIGI:
204
+ anlayan.append(yazar)
205
+ elif yuzde >= _SAHIPLIK_ESIGI:
206
+ # Anket verisi yoksa sahipliği baz al
207
+ anlayan.append(yazar)
208
+
209
+ bus_factor = max(len(anlayan), 1)
210
+ if bus_factor == 1:
211
+ risk = "KRİTİK"
212
+ elif bus_factor == 2:
213
+ risk = "RİSKLİ"
214
+ else:
215
+ risk = "GÜVENLİ"
216
+
217
+ # Birincil sahip
218
+ birincil = max(yazar_satirlari, key=lambda y: yazar_satirlari[y])
219
+ birincil_yuzde = yazar_satirlari[birincil] / toplam
220
+
221
+ # Göreli dosya yolu
222
+ try:
223
+ goreceli_yol = str(dosya.relative_to(kok))
224
+ except ValueError:
225
+ goreceli_yol = str(dosya)
226
+
227
+ sonuclar.append(
228
+ BusFaktorSonucu(
229
+ dosya_yolu=goreceli_yol,
230
+ bus_factor=bus_factor,
231
+ birincil_sahip=birincil,
232
+ sahiplik_yuzdesi=round(birincil_yuzde * 100, 1),
233
+ risk=risk,
234
+ anlayan_yazarlar=anlayan,
235
+ toplam_satir=toplam,
236
+ )
237
+ )
238
+
239
+ # Bus factor'a göre artan sıralama (KRİTİK önce)
240
+ sonuclar.sort(key=lambda s: s.bus_factor)
241
+ return sonuclar
242
+
243
+
244
+ def get_at_risk_files(
245
+ repo_path: Path,
246
+ db_path: Path,
247
+ ) -> list[BusFaktorSonucu]:
248
+ """
249
+ bus_factor == 1 olan dosyaları döndür (KRİTİK liste).
250
+
251
+ Args:
252
+ repo_path: Git repo kök dizini
253
+ db_path: SQLite veritabanı yolu
254
+
255
+ Returns:
256
+ Kritik dosyaların BusFaktorSonucu listesi
257
+ """
258
+ tum = calculate_bus_factor(repo_path, db_path)
259
+ return [s for s in tum if s.bus_factor == 1]