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/interview.py ADDED
@@ -0,0 +1,298 @@
1
+ """
2
+ Aday değerlendirme — kodbase anlama testi.
3
+
4
+ ÖNEMLİ UYARI:
5
+ Bu araç insan değerlendirmesinin YERİNE GEÇMEZ. Tamamlayıcı bir sinyaldir.
6
+ Tek başına işe alım kararı için kullanılmamalıdır.
7
+ Otomatik puanlama bu fazda eklenmiyor — insan değerlendiricisi skorу manuel girer.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import json
13
+ import re
14
+ import time
15
+ from dataclasses import dataclass
16
+ from pathlib import Path
17
+ from typing import Optional
18
+
19
+ from codedna.db import get_connection
20
+
21
+ # Zorluk → karmaşıklık skoru eşikleri
22
+ _ZORLU_ESIKLER = {
23
+ "easy": (0.0, 5.0), # düşük karmaşıklık
24
+ "medium": (5.0, 15.0), # orta karmaşıklık
25
+ "hard": (15.0, 999.), # yüksek karmaşıklık
26
+ }
27
+
28
+ # Anonimleştirme: yaygın tanımlayıcı kalıpları jenerik isimlerle değiştir
29
+ _ANONIMLESTIME_KALIPLARI: list[tuple[str, str]] = [
30
+ (r'\b(payment|charge|invoice|billing)\b', 'transaction'),
31
+ (r'\b(user|account|customer|member)\b', 'entity'),
32
+ (r'\b(password|secret|token|key|credential)\b', 'credential'),
33
+ (r'\b(database|db|repository|repo|store)\b', 'storage'),
34
+ (r'\b(email|phone|address|contact)\b', 'contact_info'),
35
+ ]
36
+
37
+
38
+ @dataclass
39
+ class MulakatDosyasi:
40
+ """Mülakat için seçilmiş dosya bilgisi."""
41
+
42
+ dosya_yolu: str
43
+ anonimlestirilmis_kod: str
44
+ karmasiklik_skoru: float
45
+ zorluk: str
46
+ satir_sayisi: int
47
+
48
+
49
+ def _kod_anonimize_et(kod: str) -> str:
50
+ """
51
+ Kaynak kodu anonimleştir — iş mantığını açık eden tanımlayıcıları
52
+ jenerik isimlerle değiştir. Kod yapısını ve mantığını korur.
53
+
54
+ Args:
55
+ kod: Ham kaynak kodu
56
+
57
+ Returns:
58
+ Anonimleştirilmiş kod
59
+ """
60
+ sonuc = kod
61
+ for kalip, yeni in _ANONIMLESTIME_KALIPLARI:
62
+ sonuc = re.sub(kalip, yeni, sonuc, flags=re.IGNORECASE)
63
+ return sonuc
64
+
65
+
66
+ def select_candidate_file(
67
+ repo_path: Path,
68
+ db_path: Path,
69
+ difficulty: str = "medium",
70
+ ) -> Optional[MulakatDosyasi]:
71
+ """
72
+ Repo'dan mülakat için uygun bir dosya seç.
73
+
74
+ Seçim kriterleri:
75
+ - İstenen zorluk aralığındaki karmaşıklık skoru
76
+ - Korumalı modüller HİÇBİR ZAMAN seçilmez
77
+ - En az 20, en fazla 200 satır
78
+ - Desteklenen uzantı (.py, .js, .ts)
79
+
80
+ Args:
81
+ repo_path: Git repo kök dizini
82
+ db_path: SQLite veritabanı yolu
83
+ difficulty: "easy" | "medium" | "hard"
84
+
85
+ Returns:
86
+ MulakatDosyasi nesnesi veya uygun dosya yoksa None
87
+ """
88
+ from codedna.scorer import scan_repository
89
+ from codedna.protection import check_protected_modules
90
+
91
+ alt_esik, ust_esik = _ZORLU_ESIKLER.get(difficulty, _ZORLU_ESIKLER["medium"])
92
+
93
+ # Korumalı modülleri al — bunlar asla seçilmez
94
+ korunanlari = {
95
+ m.dosya_yolu
96
+ for m in check_protected_modules(db_path)
97
+ }
98
+
99
+ taranan = scan_repository(repo_path, max_files=200)
100
+
101
+ uygun = [
102
+ s for s in taranan
103
+ if alt_esik <= s.complexity_score < ust_esik
104
+ and s.file_path not in korunanlari
105
+ and 20 <= s.total_lines <= 200
106
+ ]
107
+
108
+ if not uygun:
109
+ return None
110
+
111
+ # En ortanca karmaşıklığa sahip dosyayı seç (çok kolay/zor olmasın)
112
+ uygun.sort(key=lambda s: abs(s.complexity_score - (alt_esik + ust_esik) / 2))
113
+ secilen = uygun[0]
114
+
115
+ try:
116
+ kod = Path(secilen.file_path).read_text(encoding="utf-8", errors="replace")
117
+ except Exception:
118
+ return None
119
+
120
+ anonimlestirilmis = _kod_anonimize_et(kod)
121
+
122
+ return MulakatDosyasi(
123
+ dosya_yolu=secilen.file_path,
124
+ anonimlestirilmis_kod=anonimlestirilmis,
125
+ karmasiklik_skoru=round(secilen.complexity_score, 1),
126
+ zorluk=difficulty,
127
+ satir_sayisi=secilen.total_lines,
128
+ )
129
+
130
+
131
+ def generate_questions(code: str) -> list[str]:
132
+ """
133
+ Koddan otomatik 3 anlama sorusu üret.
134
+
135
+ Şablon tabanlı üretim — AI çağrısı gerektirmez, kod yapısından çıkarım.
136
+ Üretilen sorular her zaman genel, spesifik iş mantığına bağlı değil.
137
+
138
+ Args:
139
+ code: Kaynak kodu (anonimleştirilmiş olabilir)
140
+
141
+ Returns:
142
+ 3 soruluk liste
143
+ """
144
+ satirlar = code.splitlines()
145
+ fonksiyon_sayisi = sum(
146
+ 1 for s in satirlar
147
+ if re.match(r'\s*(def |function |async function )', s)
148
+ )
149
+ kos_sayisi = sum(
150
+ 1 for s in satirlar
151
+ if re.search(r'\b(if|else|elif|for|while|try|except|catch)\b', s)
152
+ )
153
+ donduruyor_mu = any("return " in s for s in satirlar)
154
+
155
+ sorular = [
156
+ "Bu kodu okuduğunuzda ana fonksiyonun/metodun birincil amacı ne?",
157
+ f"Bu kod {fonksiyon_sayisi} fonksiyon/metod içeriyor. "
158
+ f"Hangisi en kritik iş mantığını taşıyor ve neden?",
159
+ ]
160
+
161
+ if kos_sayisi > 3:
162
+ sorular.append(
163
+ f"Kodda {kos_sayisi} dal/koşul var. Hangi koşul en önemli hata senaryosunu ele alıyor?"
164
+ )
165
+ elif donduruyor_mu:
166
+ sorular.append(
167
+ "Bu fonksiyon hangi koşulda beklenmedik bir değer döndürebilir? "
168
+ "Bu durumu nasıl debug ederdiniz?"
169
+ )
170
+ else:
171
+ sorular.append(
172
+ "Bu koda bir test yazmanız gerekse, önce hangi davranışı test ederdiniz?"
173
+ )
174
+
175
+ return sorular[:3]
176
+
177
+
178
+ def start_session(
179
+ candidate_name: str,
180
+ file_path: str,
181
+ questions: list[str],
182
+ db_path: Path,
183
+ created_by: str = "system",
184
+ ) -> int:
185
+ """
186
+ Yeni mülakat oturumu başlat.
187
+
188
+ Args:
189
+ candidate_name: Aday adı
190
+ file_path: Test edilen dosyanın yolu
191
+ questions: Sorulan soru listesi
192
+ db_path: SQLite veritabanı yolu
193
+ created_by: Oturumu başlatan kişi
194
+
195
+ Returns:
196
+ Yeni oturum id'si
197
+ """
198
+ with get_connection(db_path) as conn:
199
+ cur = conn.execute(
200
+ """
201
+ INSERT INTO interview_sessions
202
+ (candidate_name, file_path, started_at, questions_json, created_by)
203
+ VALUES (?, ?, ?, ?, ?)
204
+ """,
205
+ (
206
+ candidate_name,
207
+ file_path,
208
+ int(time.time()),
209
+ json.dumps(questions, ensure_ascii=False),
210
+ created_by,
211
+ ),
212
+ )
213
+ return cur.lastrowid or 0
214
+
215
+
216
+ def submit_score(
217
+ session_id: int,
218
+ score: float,
219
+ evaluator_notes: str,
220
+ db_path: Path,
221
+ ) -> dict:
222
+ """
223
+ İnsan değerlendiricinin verdiği puanı kaydet.
224
+
225
+ Otomatik puanlama kasıtlı olarak yok — yanıltıcı olabileceğinden
226
+ bu fazda insan değerlendirmesi zorunlu tutulmuştur.
227
+
228
+ Args:
229
+ session_id: Oturum id'si
230
+ score: 0.0–5.0 arası puan
231
+ evaluator_notes: Değerlendirici notları
232
+ db_path: SQLite veritabanı yolu
233
+
234
+ Returns:
235
+ Güncellenen oturum özeti
236
+ """
237
+ if not (0.0 <= score <= 5.0):
238
+ raise ValueError(f"Skor 0.0–5.0 arasında olmalı, gelen: {score}")
239
+
240
+ su_an = int(time.time())
241
+ with get_connection(db_path) as conn:
242
+ cur = conn.execute(
243
+ """
244
+ UPDATE interview_sessions
245
+ SET comprehension_score = ?,
246
+ evaluator_notes = ?,
247
+ completed_at = ?
248
+ WHERE id = ?
249
+ """,
250
+ (score, evaluator_notes, su_an, session_id),
251
+ )
252
+ if cur.rowcount == 0:
253
+ raise ValueError(f"Oturum #{session_id} bulunamadı.")
254
+
255
+ return {
256
+ "session_id": session_id,
257
+ "comprehension_score": score,
258
+ "evaluator_notes": evaluator_notes,
259
+ "mesaj": "Değerlendirme kaydedildi.",
260
+ }
261
+
262
+
263
+ def get_sessions(db_path: Path, limit: int = 20) -> list[dict]:
264
+ """Geçmiş mülakat oturumlarını döndür."""
265
+ try:
266
+ with get_connection(db_path) as conn:
267
+ rows = conn.execute(
268
+ """
269
+ SELECT id, candidate_name, file_path, started_at, completed_at,
270
+ comprehension_score, evaluator_notes, created_by
271
+ FROM interview_sessions
272
+ ORDER BY started_at DESC
273
+ LIMIT ?
274
+ """,
275
+ (limit,),
276
+ ).fetchall()
277
+ except Exception:
278
+ return []
279
+
280
+ def _ts(ts: Optional[int]) -> Optional[str]:
281
+ if not ts:
282
+ return None
283
+ from datetime import datetime
284
+ return datetime.fromtimestamp(ts).strftime("%Y-%m-%d %H:%M")
285
+
286
+ return [
287
+ {
288
+ "id": r["id"],
289
+ "aday": r["candidate_name"],
290
+ "dosya_yolu": r["file_path"],
291
+ "baslangic": _ts(r["started_at"]),
292
+ "bitis": _ts(r["completed_at"]),
293
+ "skor": r["comprehension_score"],
294
+ "notlar": r["evaluator_notes"],
295
+ "olusturan": r["created_by"],
296
+ }
297
+ for r in rows
298
+ ]
codedna/onboarding.py ADDED
@@ -0,0 +1,195 @@
1
+ """Yeni katılan geliştiricilerin üretkenlik eğrisini ölçer."""
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
11
+
12
+ # Üretkenlik eşiği — bu skoru aşınca "ramp-up tamamlandı" sayılır
13
+ _VARSAYILAN_ESIK = 3.5
14
+
15
+ # Ramp-up tahmini için minimum commit sayısı
16
+ _MIN_COMMIT = 5
17
+
18
+
19
+ @dataclass
20
+ class CommitNoktasi:
21
+ """Tek commit için anlama skoru verisi."""
22
+
23
+ commit_hash: str
24
+ commit_no: int # yazar bazlı sıra numarası (1'den başlar)
25
+ tarih: datetime
26
+ understanding_score: Optional[float]
27
+ hafta_no: int # ilk committen itibaren geçen hafta sayısı
28
+
29
+
30
+ @dataclass
31
+ class YazarEgri:
32
+ """Tek yazar için onboarding eğrisi."""
33
+
34
+ yazar: str
35
+ toplam_commit: int
36
+ anlama_skoru_olan: int
37
+ ramp_up_hafta: Optional[float] # None = yeterli veri yok / eşik aşılmadı
38
+ son_ort_anlama: Optional[float] # son 5 commit ortalaması
39
+ noktalar: list[CommitNoktasi]
40
+
41
+
42
+ def get_author_timeline(author: str, db_path: Path) -> list[CommitNoktasi]:
43
+ """
44
+ Bir yazarın tüm commit'lerini kronolojik anlama skoruyla döndür.
45
+
46
+ Args:
47
+ author: Yazar adı (commits.author ile eşleşmeli)
48
+ db_path: SQLite veritabanı yolu
49
+
50
+ Returns:
51
+ CommitNoktasi listesi, tarih sırasıyla
52
+ """
53
+ try:
54
+ with get_connection(db_path) as conn:
55
+ rows = conn.execute(
56
+ """
57
+ SELECT commit_hash, author, timestamp, understanding_score
58
+ FROM commits
59
+ WHERE author = ?
60
+ ORDER BY timestamp ASC
61
+ """,
62
+ (author,),
63
+ ).fetchall()
64
+ except Exception:
65
+ return []
66
+
67
+ if not rows:
68
+ return []
69
+
70
+ ilk_ts = rows[0]["timestamp"] or 0
71
+ noktalar: list[CommitNoktasi] = []
72
+
73
+ for i, r in enumerate(rows):
74
+ ts = r["timestamp"] or ilk_ts
75
+ hafta = int((ts - ilk_ts) / (7 * 24 * 3600))
76
+ noktalar.append(
77
+ CommitNoktasi(
78
+ commit_hash=r["commit_hash"] or "",
79
+ commit_no=i + 1,
80
+ tarih=datetime.fromtimestamp(ts),
81
+ understanding_score=(
82
+ float(r["understanding_score"])
83
+ if r["understanding_score"] is not None
84
+ else None
85
+ ),
86
+ hafta_no=hafta,
87
+ )
88
+ )
89
+
90
+ return noktalar
91
+
92
+
93
+ def estimate_ramp_up_weeks(
94
+ author: str,
95
+ db_path: Path,
96
+ threshold: float = _VARSAYILAN_ESIK,
97
+ ) -> Optional[float]:
98
+ """
99
+ Yazarın ortalama anlama skoru threshold'u aştığı ilk haftayı tahmin et.
100
+
101
+ Algoritma:
102
+ - Anlama skoru olan commit'ler 3'lü hareketli ortalama ile yumuşatılır
103
+ - Yumuşatılmış skor threshold'u aştığı ilk commit'in hafta numarası döndürülür
104
+ - Yeterli veri yoksa (< MIN_COMMIT anketli commit) None döndürülür
105
+
106
+ Args:
107
+ author: Yazar adı
108
+ db_path: SQLite veritabanı yolu
109
+ threshold: Üretkenlik eşiği (varsayılan 3.5/5)
110
+
111
+ Returns:
112
+ Ramp-up haftası veya None
113
+ """
114
+ noktalar = get_author_timeline(author, db_path)
115
+ anketli = [n for n in noktalar if n.understanding_score is not None]
116
+
117
+ if len(anketli) < _MIN_COMMIT:
118
+ return None
119
+
120
+ # 3'lü hareketli ortalama
121
+ skorlar = [n.understanding_score for n in anketli] # type: ignore[misc]
122
+ for i in range(2, len(skorlar)):
123
+ pencere = skorlar[max(0, i - 2): i + 1]
124
+ ort = sum(pencere) / len(pencere)
125
+ if ort >= threshold:
126
+ return float(anketli[i].hafta_no)
127
+
128
+ return None # eşik hiç aşılmadı
129
+
130
+
131
+ def get_author_curve(author: str, db_path: Path) -> YazarEgri:
132
+ """
133
+ Tek yazar için tam onboarding eğrisi nesnesi oluştur.
134
+
135
+ Args:
136
+ author: Yazar adı
137
+ db_path: SQLite veritabanı yolu
138
+
139
+ Returns:
140
+ YazarEgri nesnesi
141
+ """
142
+ noktalar = get_author_timeline(author, db_path)
143
+ anketli = [n for n in noktalar if n.understanding_score is not None]
144
+
145
+ ramp_up = estimate_ramp_up_weeks(author, db_path)
146
+
147
+ son_5 = [n.understanding_score for n in anketli[-5:] if n.understanding_score]
148
+ son_ort = sum(son_5) / len(son_5) if son_5 else None
149
+
150
+ return YazarEgri(
151
+ yazar=author,
152
+ toplam_commit=len(noktalar),
153
+ anlama_skoru_olan=len(anketli),
154
+ ramp_up_hafta=round(ramp_up, 1) if ramp_up is not None else None,
155
+ son_ort_anlama=round(son_ort, 2) if son_ort is not None else None,
156
+ noktalar=noktalar,
157
+ )
158
+
159
+
160
+ def get_all_authors(db_path: Path) -> list[str]:
161
+ """DB'deki tüm benzersiz yazar listesini döndür."""
162
+ try:
163
+ with get_connection(db_path) as conn:
164
+ rows = conn.execute(
165
+ "SELECT DISTINCT author FROM commits WHERE author IS NOT NULL ORDER BY author"
166
+ ).fetchall()
167
+ return [r["author"] for r in rows]
168
+ except Exception:
169
+ return []
170
+
171
+
172
+ def team_onboarding_summary(db_path: Path) -> list[dict]:
173
+ """
174
+ Takımdaki tüm yazarlar için ramp-up süresi özeti döndür.
175
+
176
+ Returns:
177
+ Yazar bazlı özet dict listesi, ramp_up_hafta'ya göre sıralı
178
+ """
179
+ yazarlar = get_all_authors(db_path)
180
+ ozet: list[dict] = []
181
+
182
+ for yazar in yazarlar:
183
+ egri = get_author_curve(yazar, db_path)
184
+ ozet.append({
185
+ "yazar": yazar,
186
+ "toplam_commit": egri.toplam_commit,
187
+ "anlama_skoru_olan": egri.anlama_skoru_olan,
188
+ "ramp_up_hafta": egri.ramp_up_hafta,
189
+ "son_ort_anlama": egri.son_ort_anlama,
190
+ "yeterli_veri": egri.anlama_skoru_olan >= _MIN_COMMIT,
191
+ })
192
+
193
+ # Ramp-up süresi olan önce, sonra None olanlar
194
+ ozet.sort(key=lambda x: (x["ramp_up_hafta"] is None, x["ramp_up_hafta"] or 999))
195
+ return ozet
codedna/plan.py ADDED
@@ -0,0 +1,184 @@
1
+ """Plan yönetimi — lisans anahtarı ile plan seviyesi belirlenir."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from enum import Enum
7
+ from pathlib import Path
8
+ from typing import Optional
9
+
10
+
11
+ class Plan(str, Enum):
12
+ """Kullanılabilir plan seviyeleri."""
13
+
14
+ FREE = "free"
15
+ PRO = "pro"
16
+ TEAM = "team"
17
+ ENTERPRISE = "enterprise"
18
+
19
+
20
+ # Lisans dosyası yolu
21
+ _LISANS_YOLU = Path.home() / ".codedna" / "license.json"
22
+
23
+ # Özellik → izin verilen planlar eşlemesi
24
+ _OZELLIK_PLANLARI: dict[str, list[Plan]] = {
25
+ "unlimited_repos": [Plan.PRO, Plan.TEAM, Plan.ENTERPRISE],
26
+ "unlimited_files": [Plan.PRO, Plan.TEAM, Plan.ENTERPRISE],
27
+ "history_90d": [Plan.PRO, Plan.TEAM, Plan.ENTERPRISE],
28
+ "history_365d": [Plan.TEAM, Plan.ENTERPRISE],
29
+ "dashboard_access": [Plan.PRO, Plan.TEAM, Plan.ENTERPRISE],
30
+ "github_actions": [Plan.PRO, Plan.TEAM, Plan.ENTERPRISE],
31
+ "slack_notify": [Plan.TEAM, Plan.ENTERPRISE],
32
+ "bus_factor": [Plan.TEAM, Plan.ENTERPRISE],
33
+ "sprint_health": [Plan.TEAM, Plan.ENTERPRISE],
34
+ "ai_comparison": [Plan.ENTERPRISE],
35
+ "interview_tool": [Plan.ENTERPRISE],
36
+ # NatureCo CLI entegrasyonu — Pro+ özellik
37
+ "natureco_integration": [Plan.PRO, Plan.TEAM, Plan.ENTERPRISE],
38
+ }
39
+
40
+ # Tüm planlarda erişilebilen özellikler (varsayılan)
41
+ _HERKESE_ACIK: set[str] = {
42
+ "scan", "history_7d", "cli_basic",
43
+ }
44
+
45
+
46
+ def get_current_plan() -> Plan:
47
+ """
48
+ ~/.codedna/license.json dosyasından plan oku.
49
+
50
+ Dosya yoksa FREE döndür.
51
+ Format: {"plan": "pro", "key": "...", "expires": "2027-01-01"}
52
+ """
53
+ if not _LISANS_YOLU.exists():
54
+ return Plan.FREE
55
+ try:
56
+ veri = json.loads(_LISANS_YOLU.read_text(encoding="utf-8"))
57
+ return Plan(veri.get("plan", "free"))
58
+ except Exception:
59
+ return Plan.FREE
60
+
61
+
62
+ def is_feature_available(feature: str, plan: Optional[Plan] = None) -> bool:
63
+ """
64
+ Verilen özelliğin mevcut planda kullanılabilir olup olmadığını kontrol et.
65
+
66
+ Args:
67
+ feature: Kontrol edilecek özellik adı
68
+ plan: Plan seviyesi (None ise mevcut plan okunur)
69
+
70
+ Returns:
71
+ Özellik kullanılabilirse True
72
+ """
73
+ if plan is None:
74
+ plan = get_current_plan()
75
+
76
+ # Herkese açık özellikler
77
+ if feature in _HERKESE_ACIK:
78
+ return True
79
+
80
+ return plan in _OZELLIK_PLANLARI.get(
81
+ feature,
82
+ [Plan.FREE, Plan.PRO, Plan.TEAM, Plan.ENTERPRISE],
83
+ )
84
+
85
+
86
+ def activate_license(key: str) -> Plan:
87
+ """
88
+ Lisans anahtarını doğrula ve aktif et.
89
+
90
+ Gerçek doğrulama Faz 9'da API üzerinden yapılacak.
91
+ Şimdilik anahtar formatına göre plan belirler:
92
+ - 'CDNA-PRO-...' → pro
93
+ - 'CDNA-TEAM-...' → team
94
+ - 'CDNA-ENT-...' → enterprise
95
+
96
+ Args:
97
+ key: Lisans anahtarı
98
+
99
+ Returns:
100
+ Aktif edilen plan seviyesi
101
+ """
102
+ anahtar = key.upper().strip()
103
+ if anahtar.startswith("CDNA-PRO"):
104
+ plan = Plan.PRO
105
+ elif anahtar.startswith("CDNA-TEAM"):
106
+ plan = Plan.TEAM
107
+ elif anahtar.startswith("CDNA-ENT"):
108
+ plan = Plan.ENTERPRISE
109
+ else:
110
+ raise ValueError(f"Geçersiz lisans anahtarı formatı: {key!r}")
111
+
112
+ # Lisans dosyasını yaz
113
+ _LISANS_YOLU.parent.mkdir(parents=True, exist_ok=True)
114
+ _LISANS_YOLU.write_text(
115
+ json.dumps({"plan": plan.value, "key": anahtar}, ensure_ascii=False, indent=2),
116
+ encoding="utf-8",
117
+ )
118
+ return plan
119
+
120
+
121
+ def activate_demo_license(plan: Plan) -> Plan:
122
+ """
123
+ Demo lisansı aktif et — gerçek key gerekmeden.
124
+
125
+ Development, demo, CI/CD ve test için.
126
+ Süresi 7 gün, sonra tekrar `codedna plan demo` ile uzatılabilir.
127
+
128
+ Args:
129
+ plan: Hedef plan (free, pro, team, enterprise)
130
+
131
+ Returns:
132
+ Aktif edilen plan
133
+ """
134
+ import secrets as _secrets
135
+
136
+ su_an = int(__import__("time").time())
137
+ gecerli = su_an + (7 * 24 * 3600) # 7 gün
138
+
139
+ # Rastgele demo key (gerçek formatı taklit eder)
140
+ demo_key = f"CDNA-{plan.value.upper()[:3]}-DEMO-{_secrets.token_urlsafe(16).upper()}"
141
+
142
+ _LISANS_YOLU.parent.mkdir(parents=True, exist_ok=True)
143
+ _LISANS_YOLU.write_text(
144
+ json.dumps(
145
+ {
146
+ "plan": plan.value,
147
+ "key": demo_key,
148
+ "expires": __import__("datetime").datetime.fromtimestamp(gecerli).strftime("%Y-%m-%d"),
149
+ "demo": True,
150
+ "activated_at": su_an,
151
+ },
152
+ ensure_ascii=False,
153
+ indent=2,
154
+ ),
155
+ encoding="utf-8",
156
+ )
157
+ return plan
158
+
159
+
160
+ def get_plan_limits() -> dict[str, object]:
161
+ """Mevcut planın limit değerlerini döndür."""
162
+ plan = get_current_plan()
163
+ limitler: dict[str, object] = {
164
+ Plan.FREE: {
165
+ "max_repos": 1, "max_files_scan": 50, "history_days": 7,
166
+ "dashboard_access": False, "github_actions": False,
167
+ },
168
+ Plan.PRO: {
169
+ "max_repos": -1, "max_files_scan": -1, "history_days": 90,
170
+ "dashboard_access": True, "github_actions": True,
171
+ },
172
+ Plan.TEAM: {
173
+ "max_repos": -1, "max_files_scan": -1, "history_days": 365,
174
+ "dashboard_access": True, "github_actions": True,
175
+ "slack_notify": True, "bus_factor": True,
176
+ },
177
+ Plan.ENTERPRISE: {
178
+ "max_repos": -1, "max_files_scan": -1, "history_days": -1,
179
+ "dashboard_access": True, "github_actions": True,
180
+ "slack_notify": True, "bus_factor": True,
181
+ "sprint_health": True, "ai_comparison": True,
182
+ },
183
+ }
184
+ return limitler.get(plan, limitler[Plan.FREE]) # type: ignore[return-value]