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/cli.py
ADDED
|
@@ -0,0 +1,1965 @@
|
|
|
1
|
+
"""CodeDNA CLI — typer tabanlı komut satırı arayüzü."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
import typer
|
|
10
|
+
from rich.console import Console
|
|
11
|
+
from rich.panel import Panel
|
|
12
|
+
from rich.table import Table
|
|
13
|
+
from rich.text import Text
|
|
14
|
+
|
|
15
|
+
from codedna import __version__
|
|
16
|
+
from codedna.db import (
|
|
17
|
+
get_db_path,
|
|
18
|
+
get_all_file_understanding_scores,
|
|
19
|
+
get_commit_history,
|
|
20
|
+
get_file_scores_for_commit,
|
|
21
|
+
get_latest_commit,
|
|
22
|
+
init_db,
|
|
23
|
+
save_commit,
|
|
24
|
+
save_file_score,
|
|
25
|
+
update_understanding_score,
|
|
26
|
+
)
|
|
27
|
+
from codedna.git_hook import find_git_root, install_hook, install_ci_workflow, is_hook_installed, uninstall_hook
|
|
28
|
+
from codedna.scorer import get_repo, scan_repository, score_latest_commit
|
|
29
|
+
from codedna.survey import run_survey
|
|
30
|
+
|
|
31
|
+
app = typer.Typer(
|
|
32
|
+
name="codedna",
|
|
33
|
+
help="🧬 CodeDNA — AI kod şeffaflık aracı",
|
|
34
|
+
add_completion=False,
|
|
35
|
+
no_args_is_help=True,
|
|
36
|
+
)
|
|
37
|
+
console = Console()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _get_db(repo_path: Optional[Path] = None) -> Path:
|
|
41
|
+
"""Repo'ya özel veritabanı yolunu döndür."""
|
|
42
|
+
kok = repo_path or find_git_root() or Path.cwd()
|
|
43
|
+
return get_db_path(kok)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# ---------------------------------------------------------------------------
|
|
47
|
+
# codedna init
|
|
48
|
+
# ---------------------------------------------------------------------------
|
|
49
|
+
@app.command()
|
|
50
|
+
def init(
|
|
51
|
+
repo: Optional[Path] = typer.Option(None, "--repo", "-r", help="Git repo dizini"),
|
|
52
|
+
with_ci: bool = typer.Option(False, "--with-ci", help="GitHub Actions CI şablonunu da oluştur"),
|
|
53
|
+
) -> None:
|
|
54
|
+
"""Git hook'u kur, veritabanını oluştur ve isteğe bağlı CI şablonu yaz."""
|
|
55
|
+
console.print()
|
|
56
|
+
console.print(
|
|
57
|
+
Panel.fit(
|
|
58
|
+
"[bold cyan]🧬 CodeDNA[/bold cyan] kurulumu başlatılıyor...",
|
|
59
|
+
border_style="cyan",
|
|
60
|
+
)
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
# Repo kökünü bul
|
|
64
|
+
kok = repo or find_git_root()
|
|
65
|
+
if not kok:
|
|
66
|
+
console.print("[bold red]Hata:[/bold red] Git repo bulunamadı. Önce 'git init' komutunu çalıştırın.")
|
|
67
|
+
raise typer.Exit(1)
|
|
68
|
+
|
|
69
|
+
console.print(f"[dim]Repo kökü:[/dim] {kok}")
|
|
70
|
+
|
|
71
|
+
# Veritabanını başlat
|
|
72
|
+
db_yolu = _get_db(kok)
|
|
73
|
+
init_db(db_yolu)
|
|
74
|
+
console.print(f"[green]✓[/green] Veritabanı oluşturuldu: [dim]{db_yolu}[/dim]")
|
|
75
|
+
|
|
76
|
+
# Hook'u kur
|
|
77
|
+
if is_hook_installed(kok):
|
|
78
|
+
console.print("[yellow]⚠[/yellow] Post-commit hook zaten kurulu.")
|
|
79
|
+
else:
|
|
80
|
+
if install_hook(kok):
|
|
81
|
+
console.print("[green]✓[/green] Post-commit hook kuruldu.")
|
|
82
|
+
else:
|
|
83
|
+
console.print("[red]✗[/red] Hook kurulumu başarısız.")
|
|
84
|
+
raise typer.Exit(1)
|
|
85
|
+
|
|
86
|
+
# CI şablonu
|
|
87
|
+
if with_ci:
|
|
88
|
+
install_ci_workflow(kok)
|
|
89
|
+
|
|
90
|
+
bilgi = (
|
|
91
|
+
"[bold green]CodeDNA başarıyla kuruldu![/bold green]\n"
|
|
92
|
+
"Artık her [bold]git commit[/bold] sonrası otomatik analiz çalışacak.\n\n"
|
|
93
|
+
"[dim]• Tüm repoyu taramak için:[/dim] [cyan]codedna scan[/cyan]\n"
|
|
94
|
+
"[dim]• Son commit skorunu görmek için:[/dim] [cyan]codedna status[/cyan]\n"
|
|
95
|
+
"[dim]• Geçmiş skorları görmek için:[/dim] [cyan]codedna history[/cyan]\n"
|
|
96
|
+
"[dim]• API sunucuyu başlatmak için:[/dim] [cyan]codedna serve[/cyan]\n"
|
|
97
|
+
"[dim]• HTML rapor üretmek için:[/dim] [cyan]codedna report[/cyan]"
|
|
98
|
+
)
|
|
99
|
+
if with_ci:
|
|
100
|
+
bilgi += "\n[dim]• CI şablonu:[/dim] [cyan].github/workflows/codedna.yml[/cyan]"
|
|
101
|
+
|
|
102
|
+
console.print()
|
|
103
|
+
console.print(Panel(bilgi, border_style="green", padding=(1, 2)))
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
# ---------------------------------------------------------------------------
|
|
107
|
+
# codedna scan
|
|
108
|
+
# ---------------------------------------------------------------------------
|
|
109
|
+
@app.command()
|
|
110
|
+
def scan(
|
|
111
|
+
repo: Optional[Path] = typer.Option(None, "--repo", "-r", help="Git repo dizini"),
|
|
112
|
+
max_files: int = typer.Option(200, "--max", "-m", help="Maksimum taranacak dosya sayısı"),
|
|
113
|
+
min_risk: float = typer.Option(0.0, "--min-risk", help="Minimum AI olasılığı filtresi (0.0-1.0)"),
|
|
114
|
+
) -> None:
|
|
115
|
+
"""Mevcut repo'yu tara ve AI risk raporu göster."""
|
|
116
|
+
console.print()
|
|
117
|
+
console.print("[bold cyan]🧬 CodeDNA[/bold cyan] — Repo taranıyor...\n")
|
|
118
|
+
|
|
119
|
+
kok = repo or find_git_root() or Path.cwd()
|
|
120
|
+
|
|
121
|
+
with console.status("[dim]Dosyalar analiz ediliyor...[/dim]"):
|
|
122
|
+
sonuclar = scan_repository(kok, max_files=max_files)
|
|
123
|
+
|
|
124
|
+
if not sonuclar:
|
|
125
|
+
console.print("[yellow]Taranacak desteklenen dosya bulunamadı.[/yellow]")
|
|
126
|
+
console.print("[dim]Desteklenen: .py .js .jsx .ts .tsx[/dim]")
|
|
127
|
+
return
|
|
128
|
+
|
|
129
|
+
# Min risk filtresi uygula
|
|
130
|
+
if min_risk > 0:
|
|
131
|
+
sonuclar = [s for s in sonuclar if s.ai_probability >= min_risk]
|
|
132
|
+
|
|
133
|
+
# AI olasılığına göre sırala (yüksekten düşüğe)
|
|
134
|
+
sonuclar.sort(key=lambda s: s.ai_probability, reverse=True)
|
|
135
|
+
|
|
136
|
+
# DB'den tüm dosya anlama skorlarını tek sorguda çek
|
|
137
|
+
db_yolu = _get_db(kok)
|
|
138
|
+
anlama_skorlari_map: dict[str, float] = {}
|
|
139
|
+
try:
|
|
140
|
+
anlama_skorlari_map = get_all_file_understanding_scores(db_path=db_yolu)
|
|
141
|
+
except Exception:
|
|
142
|
+
pass
|
|
143
|
+
|
|
144
|
+
# Tablo oluştur
|
|
145
|
+
tablo = Table(
|
|
146
|
+
title="",
|
|
147
|
+
border_style="dim",
|
|
148
|
+
show_lines=True,
|
|
149
|
+
header_style="bold",
|
|
150
|
+
)
|
|
151
|
+
tablo.add_column("Dosya", style="white", min_width=25)
|
|
152
|
+
tablo.add_column("AI Olasılığı", justify="center", min_width=14)
|
|
153
|
+
tablo.add_column("Karmaşıklık", justify="center", min_width=12)
|
|
154
|
+
tablo.add_column("Satır", justify="right", min_width=6)
|
|
155
|
+
tablo.add_column("Anlama Skoru", justify="center", min_width=14)
|
|
156
|
+
|
|
157
|
+
toplam_ai = 0.0
|
|
158
|
+
for s in sonuclar:
|
|
159
|
+
yuzde = int(s.ai_probability * 100)
|
|
160
|
+
ai_metin = f"{s.ai_color} %{yuzde}"
|
|
161
|
+
|
|
162
|
+
if s.complexity_label == "Yüksek":
|
|
163
|
+
karmasiklik = "[red]Yüksek[/red]"
|
|
164
|
+
elif s.complexity_label == "Orta":
|
|
165
|
+
karmasiklik = "[yellow]Orta[/yellow]"
|
|
166
|
+
else:
|
|
167
|
+
karmasiklik = "[green]Düşük[/green]"
|
|
168
|
+
|
|
169
|
+
# DB'den tek sorguda gelen map'ten anlama skorunu oku
|
|
170
|
+
anlama_skor = anlama_skorlari_map.get(s.file_path)
|
|
171
|
+
if anlama_skor is not None:
|
|
172
|
+
renk = "green" if anlama_skor >= 4.0 else "yellow" if anlama_skor >= 2.5 else "red"
|
|
173
|
+
anlama = f"[{renk}]✅ {anlama_skor:.1f}/5[/{renk}]"
|
|
174
|
+
else:
|
|
175
|
+
anlama = "[dim]⚠️ Bilinmiyor[/dim]"
|
|
176
|
+
|
|
177
|
+
goreceli = _kisalt_yol(s.file_path, str(kok))
|
|
178
|
+
|
|
179
|
+
tablo.add_row(goreceli, ai_metin, karmasiklik, str(s.total_lines), anlama)
|
|
180
|
+
toplam_ai += s.ai_probability
|
|
181
|
+
|
|
182
|
+
console.print(tablo)
|
|
183
|
+
|
|
184
|
+
# Özet satırı
|
|
185
|
+
ortalama_ai = (toplam_ai / len(sonuclar)) * 100 if sonuclar else 0
|
|
186
|
+
risk_etiketi, risk_renk = _risk_etiketi(ortalama_ai)
|
|
187
|
+
|
|
188
|
+
console.print(
|
|
189
|
+
f"\n[bold]Repo Özeti:[/bold] {len(sonuclar)} dosya tarandı · "
|
|
190
|
+
f"Ortalama AI olasılığı: [bold]%{ortalama_ai:.0f}[/bold] · "
|
|
191
|
+
f"Risk: [bold {risk_renk}]{risk_etiketi}[/bold {risk_renk}]\n"
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
# ---------------------------------------------------------------------------
|
|
196
|
+
# codedna status
|
|
197
|
+
# ---------------------------------------------------------------------------
|
|
198
|
+
@app.command()
|
|
199
|
+
def status(
|
|
200
|
+
repo: Optional[Path] = typer.Option(None, "--repo", "-r", help="Git repo dizini"),
|
|
201
|
+
hook: bool = typer.Option(False, "--hook", hidden=True, help="Hook modunda çalış (anket sor)"),
|
|
202
|
+
) -> None:
|
|
203
|
+
"""Son commit'in skorunu göster (ve hook modunda anket sor)."""
|
|
204
|
+
console.print()
|
|
205
|
+
kok = repo or find_git_root() or Path.cwd()
|
|
206
|
+
db_yolu = _get_db(kok)
|
|
207
|
+
|
|
208
|
+
# DB yoksa init et
|
|
209
|
+
init_db(db_yolu)
|
|
210
|
+
|
|
211
|
+
git_repo = get_repo(kok)
|
|
212
|
+
if not git_repo:
|
|
213
|
+
console.print("[bold red]Hata:[/bold red] Git repo bulunamadı.")
|
|
214
|
+
raise typer.Exit(1)
|
|
215
|
+
|
|
216
|
+
with console.status("[dim]Son commit analiz ediliyor...[/dim]"):
|
|
217
|
+
commit_hash, sonuclar = score_latest_commit(kok)
|
|
218
|
+
|
|
219
|
+
if not commit_hash:
|
|
220
|
+
console.print("[yellow]Henüz commit bulunamadı.[/yellow]")
|
|
221
|
+
return
|
|
222
|
+
|
|
223
|
+
# Commit bilgilerini al
|
|
224
|
+
try:
|
|
225
|
+
commit = git_repo.head.commit
|
|
226
|
+
yazar = f"{commit.author.name}"
|
|
227
|
+
zaman_dam = int(commit.committed_date)
|
|
228
|
+
mesaj = commit.message.strip().splitlines()[0][:60]
|
|
229
|
+
except Exception:
|
|
230
|
+
yazar = "Bilinmiyor"
|
|
231
|
+
zaman_dam = 0
|
|
232
|
+
mesaj = ""
|
|
233
|
+
|
|
234
|
+
# Anket (sadece hook modunda)
|
|
235
|
+
anlama_skoru: Optional[float] = None
|
|
236
|
+
if hook:
|
|
237
|
+
anlama_skoru = run_survey(commit_hash)
|
|
238
|
+
|
|
239
|
+
# DB'ye kaydet
|
|
240
|
+
save_commit(
|
|
241
|
+
commit_hash=commit_hash,
|
|
242
|
+
author=yazar,
|
|
243
|
+
timestamp=zaman_dam,
|
|
244
|
+
files_changed=len(sonuclar),
|
|
245
|
+
understanding_score=anlama_skoru,
|
|
246
|
+
db_path=db_yolu,
|
|
247
|
+
)
|
|
248
|
+
for s in sonuclar:
|
|
249
|
+
save_file_score(
|
|
250
|
+
commit_hash=commit_hash,
|
|
251
|
+
file_path=s.file_path,
|
|
252
|
+
ai_probability=s.ai_probability,
|
|
253
|
+
complexity_score=s.complexity_score,
|
|
254
|
+
comment_ratio=s.comment_ratio,
|
|
255
|
+
understanding_score=anlama_skoru,
|
|
256
|
+
db_path=db_yolu,
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
# Özet göster
|
|
260
|
+
if sonuclar:
|
|
261
|
+
ort_ai = sum(s.ai_probability for s in sonuclar) / len(sonuclar)
|
|
262
|
+
risk_etiketi, risk_renk = _risk_etiketi(ort_ai * 100)
|
|
263
|
+
|
|
264
|
+
anlama_goster = (
|
|
265
|
+
f"[bold green]{anlama_skoru:.1f}/5[/bold green]"
|
|
266
|
+
if anlama_skoru is not None
|
|
267
|
+
else "[dim]Anket yapılmadı[/dim]"
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
console.print(
|
|
271
|
+
Panel(
|
|
272
|
+
f"[bold]Commit:[/bold] [dim]{commit_hash[:8]}[/dim] [dim]{mesaj}[/dim]\n"
|
|
273
|
+
f"[bold]Yazar:[/bold] {yazar}\n"
|
|
274
|
+
f"[bold]Değişen kod dosyaları:[/bold] {len(sonuclar)}\n"
|
|
275
|
+
f"[bold]Ort. AI olasılığı:[/bold] [bold {risk_renk}]%{ort_ai*100:.0f} ({risk_etiketi})[/bold {risk_renk}]\n"
|
|
276
|
+
f"[bold]Anlama skoru:[/bold] {anlama_goster}",
|
|
277
|
+
title="[bold cyan]🧬 CodeDNA — Commit Skoru[/bold cyan]",
|
|
278
|
+
border_style="cyan",
|
|
279
|
+
padding=(1, 2),
|
|
280
|
+
)
|
|
281
|
+
)
|
|
282
|
+
console.print("[dim]Commit skoru kaydedildi.[/dim]\n")
|
|
283
|
+
else:
|
|
284
|
+
console.print(
|
|
285
|
+
f"[bold]Commit:[/bold] [dim]{commit_hash[:8]}[/dim]\n"
|
|
286
|
+
"[dim]Bu commit'te desteklenen kod dosyası bulunamadı.[/dim]"
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
# Post-commit hook: korumalı modül ihlallerini kontrol et ve uyar (bloklama YOK)
|
|
290
|
+
if hook:
|
|
291
|
+
try:
|
|
292
|
+
from codedna.protection import ihlal_uyarisi_goster
|
|
293
|
+
uyarilar = ihlal_uyarisi_goster(db_yolu)
|
|
294
|
+
for uyari in uyarilar:
|
|
295
|
+
console.print(f"[bold red]{uyari}[/bold red]")
|
|
296
|
+
except Exception:
|
|
297
|
+
pass # Hook'u asla bozmaz
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
# ---------------------------------------------------------------------------
|
|
301
|
+
# codedna history
|
|
302
|
+
# ---------------------------------------------------------------------------
|
|
303
|
+
@app.command()
|
|
304
|
+
def history(
|
|
305
|
+
repo: Optional[Path] = typer.Option(None, "--repo", "-r", help="Git repo dizini"),
|
|
306
|
+
limit: int = typer.Option(20, "--limit", "-n", help="Gösterilecek commit sayısı"),
|
|
307
|
+
) -> None:
|
|
308
|
+
"""Geçmiş commit skorlarını tablo olarak göster."""
|
|
309
|
+
console.print()
|
|
310
|
+
kok = repo or find_git_root() or Path.cwd()
|
|
311
|
+
db_yolu = _get_db(kok)
|
|
312
|
+
|
|
313
|
+
init_db(db_yolu)
|
|
314
|
+
satirlar = get_commit_history(limit=limit, db_path=db_yolu)
|
|
315
|
+
|
|
316
|
+
if not satirlar:
|
|
317
|
+
console.print(
|
|
318
|
+
"[yellow]Henüz kayıtlı commit yok.[/yellow]\n"
|
|
319
|
+
"[dim]İpucu: 'codedna init' ile kurulum yapın, ardından commit atın.[/dim]"
|
|
320
|
+
)
|
|
321
|
+
return
|
|
322
|
+
|
|
323
|
+
tablo = Table(
|
|
324
|
+
title=f"[bold cyan]🧬 CodeDNA — Son {len(satirlar)} Commit[/bold cyan]",
|
|
325
|
+
border_style="dim",
|
|
326
|
+
show_lines=True,
|
|
327
|
+
header_style="bold",
|
|
328
|
+
)
|
|
329
|
+
tablo.add_column("Commit", style="dim", min_width=10)
|
|
330
|
+
tablo.add_column("Yazar", min_width=15)
|
|
331
|
+
tablo.add_column("Tarih", min_width=17)
|
|
332
|
+
tablo.add_column("Kod Dosyası", justify="right", min_width=11)
|
|
333
|
+
tablo.add_column("Anlama", justify="center", min_width=12)
|
|
334
|
+
|
|
335
|
+
for satir in satirlar:
|
|
336
|
+
tarih_str = (
|
|
337
|
+
datetime.fromtimestamp(satir["timestamp"]).strftime("%Y-%m-%d %H:%M")
|
|
338
|
+
if satir["timestamp"]
|
|
339
|
+
else "?"
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
if satir["understanding_score"] is not None:
|
|
343
|
+
skor = satir["understanding_score"]
|
|
344
|
+
if skor >= 4.0:
|
|
345
|
+
anlama = f"[green]✅ {skor:.1f}/5[/green]"
|
|
346
|
+
elif skor >= 2.5:
|
|
347
|
+
anlama = f"[yellow]🔶 {skor:.1f}/5[/yellow]"
|
|
348
|
+
else:
|
|
349
|
+
anlama = f"[red]🔴 {skor:.1f}/5[/red]"
|
|
350
|
+
else:
|
|
351
|
+
anlama = "[dim]⚠️ Yok[/dim]"
|
|
352
|
+
|
|
353
|
+
tablo.add_row(
|
|
354
|
+
satir["commit_hash"][:8],
|
|
355
|
+
satir["author"] or "?",
|
|
356
|
+
tarih_str,
|
|
357
|
+
str(satir["files_changed"] or 0),
|
|
358
|
+
anlama,
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
console.print(tablo)
|
|
362
|
+
console.print()
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
# ---------------------------------------------------------------------------
|
|
366
|
+
# codedna serve
|
|
367
|
+
# ---------------------------------------------------------------------------
|
|
368
|
+
@app.command()
|
|
369
|
+
def serve(
|
|
370
|
+
host: str = typer.Option("127.0.0.1", "--host", help="Dinlenecek IP adresi"),
|
|
371
|
+
port: int = typer.Option(8000, "--port", "-p", help="Port numarası"),
|
|
372
|
+
reload: bool = typer.Option(False, "--reload", help="Geliştirme modunda otomatik yeniden başlat"),
|
|
373
|
+
repo: Optional[Path] = typer.Option(None, "--repo", "-r", help="Git repo dizini"),
|
|
374
|
+
) -> None:
|
|
375
|
+
"""FastAPI REST sunucusunu başlat."""
|
|
376
|
+
import os
|
|
377
|
+
import uvicorn
|
|
378
|
+
|
|
379
|
+
kok = repo or find_git_root() or Path.cwd()
|
|
380
|
+
db_yolu = _get_db(kok)
|
|
381
|
+
|
|
382
|
+
# Ortam değişkenlerini ayarla (api.py tarafından okunur)
|
|
383
|
+
os.environ.setdefault("CODEDNA_REPO_PATH", str(kok))
|
|
384
|
+
os.environ.setdefault("CODEDNA_DB_PATH", str(db_yolu))
|
|
385
|
+
|
|
386
|
+
init_db(db_yolu)
|
|
387
|
+
|
|
388
|
+
console.print()
|
|
389
|
+
console.print(
|
|
390
|
+
Panel(
|
|
391
|
+
f"[bold cyan]🧬 CodeDNA API[/bold cyan] başlatılıyor...\n\n"
|
|
392
|
+
f"[bold]Adres:[/bold] [link]http://{host}:{port}[/link]\n"
|
|
393
|
+
f"[bold]Docs:[/bold] [link]http://{host}:{port}/docs[/link]\n"
|
|
394
|
+
f"[bold]Repo:[/bold] [dim]{kok}[/dim]\n"
|
|
395
|
+
f"[bold]Veritabanı:[/bold] [dim]{db_yolu}[/dim]\n\n"
|
|
396
|
+
"[dim]Durdurmak için Ctrl+C[/dim]",
|
|
397
|
+
border_style="cyan",
|
|
398
|
+
padding=(1, 2),
|
|
399
|
+
)
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
uvicorn.run(
|
|
403
|
+
"codedna.api:app",
|
|
404
|
+
host=host,
|
|
405
|
+
port=port,
|
|
406
|
+
reload=reload,
|
|
407
|
+
log_level="info",
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
# ---------------------------------------------------------------------------
|
|
412
|
+
# codedna report
|
|
413
|
+
# ---------------------------------------------------------------------------
|
|
414
|
+
@app.command()
|
|
415
|
+
def report(
|
|
416
|
+
output: Path = typer.Option(Path("codedna-report.html"), "--output", "-o", help="Çıktı dosyası"),
|
|
417
|
+
repo: Optional[Path] = typer.Option(None, "--repo", "-r", help="Git repo dizini"),
|
|
418
|
+
open_browser: bool = typer.Option(False, "--open", help="Raporu tarayıcıda aç"),
|
|
419
|
+
) -> None:
|
|
420
|
+
"""HTML rapor oluştur."""
|
|
421
|
+
import webbrowser
|
|
422
|
+
from datetime import datetime
|
|
423
|
+
from codedna.api import _rapor_html_olustur
|
|
424
|
+
|
|
425
|
+
kok = repo or find_git_root() or Path.cwd()
|
|
426
|
+
db_yolu = _get_db(kok)
|
|
427
|
+
init_db(db_yolu)
|
|
428
|
+
|
|
429
|
+
console.print()
|
|
430
|
+
console.print("[bold cyan]🧬 CodeDNA[/bold cyan] — HTML rapor oluşturuluyor...\n")
|
|
431
|
+
|
|
432
|
+
with console.status("[dim]Dosyalar analiz ediliyor...[/dim]"):
|
|
433
|
+
sonuclar = scan_repository(kok, max_files=200)
|
|
434
|
+
|
|
435
|
+
sonuclar.sort(key=lambda s: s.ai_probability, reverse=True)
|
|
436
|
+
commitler = get_commit_history(limit=50, db_path=db_yolu)
|
|
437
|
+
|
|
438
|
+
tum_ai = [s.ai_probability for s in sonuclar]
|
|
439
|
+
ort_ai = sum(tum_ai) / len(tum_ai) if tum_ai else 0.0
|
|
440
|
+
risk_etkt, _ = _risk_etiketi(ort_ai * 100)
|
|
441
|
+
|
|
442
|
+
anlama_skorlari = [
|
|
443
|
+
float(c["understanding_score"])
|
|
444
|
+
for c in commitler
|
|
445
|
+
if c["understanding_score"] is not None
|
|
446
|
+
]
|
|
447
|
+
ort_anlama = sum(anlama_skorlari) / len(anlama_skorlari) if anlama_skorlari else None
|
|
448
|
+
|
|
449
|
+
html = _rapor_html_olustur(
|
|
450
|
+
repo_adi=kok.name,
|
|
451
|
+
toplam_dosya=len(sonuclar),
|
|
452
|
+
ort_ai=ort_ai,
|
|
453
|
+
risk=risk_etkt,
|
|
454
|
+
ort_anlama=ort_anlama,
|
|
455
|
+
toplam_commit=len(commitler),
|
|
456
|
+
dosyalar=sonuclar,
|
|
457
|
+
commitler=commitler,
|
|
458
|
+
kok=kok,
|
|
459
|
+
)
|
|
460
|
+
|
|
461
|
+
output.write_text(html, encoding="utf-8")
|
|
462
|
+
console.print(f"[green]✓[/green] Rapor oluşturuldu: [bold]{output.resolve()}[/bold]")
|
|
463
|
+
console.print(
|
|
464
|
+
f" [dim]{len(sonuclar)} dosya · "
|
|
465
|
+
f"Ort. AI: %{ort_ai*100:.0f} · "
|
|
466
|
+
f"Risk: {risk_etkt}[/dim]"
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
if open_browser:
|
|
470
|
+
webbrowser.open(output.resolve().as_uri())
|
|
471
|
+
console.print("[dim]Tarayıcıda açılıyor...[/dim]")
|
|
472
|
+
|
|
473
|
+
console.print()
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
# ---------------------------------------------------------------------------
|
|
477
|
+
# codedna ai-compare
|
|
478
|
+
# ---------------------------------------------------------------------------
|
|
479
|
+
@app.command(name="ai-compare")
|
|
480
|
+
def ai_compare(
|
|
481
|
+
repo: Optional[Path] = typer.Option(None, "--repo", "-r", help="Git repo dizini"),
|
|
482
|
+
) -> None:
|
|
483
|
+
"""Repo genelinde AI araç bazlı karşılaştırma tablosu göster."""
|
|
484
|
+
from codedna.ai_fingerprint import compare_tools_in_repo
|
|
485
|
+
from codedna.plan import is_feature_available
|
|
486
|
+
|
|
487
|
+
if not is_feature_available("ai_comparison"):
|
|
488
|
+
console.print(
|
|
489
|
+
Panel(
|
|
490
|
+
"[bold yellow]🔒 Bu özellik Enterprise planında mevcut.[/bold yellow]\n"
|
|
491
|
+
"[dim]This feature is available on Enterprise plan.[/dim]\n\n"
|
|
492
|
+
"[dim]Yükseltmek için:[/dim] [cyan]codedna plan activate <LICENSE_KEY>[/cyan]",
|
|
493
|
+
border_style="yellow",
|
|
494
|
+
padding=(1, 2),
|
|
495
|
+
)
|
|
496
|
+
)
|
|
497
|
+
raise typer.Exit(1)
|
|
498
|
+
|
|
499
|
+
kok = repo or find_git_root() or Path.cwd()
|
|
500
|
+
db_yolu = _get_db(kok)
|
|
501
|
+
init_db(db_yolu)
|
|
502
|
+
|
|
503
|
+
console.print()
|
|
504
|
+
console.print("[bold cyan]🧬 CodeDNA[/bold cyan] — AI araç parmak izi analizi çalışıyor...\n")
|
|
505
|
+
console.print(
|
|
506
|
+
"[dim]⚠️ Bu tespit örüntü tabanlı bir tahmindir — kesin değildir.[/dim]\n"
|
|
507
|
+
)
|
|
508
|
+
|
|
509
|
+
with console.status("[dim]Dosyalar analiz ediliyor...[/dim]"):
|
|
510
|
+
sonuclar = compare_tools_in_repo(kok, db_yolu)
|
|
511
|
+
|
|
512
|
+
if not sonuclar:
|
|
513
|
+
console.print("[yellow]Analiz edilecek dosya bulunamadı.[/yellow]")
|
|
514
|
+
return
|
|
515
|
+
|
|
516
|
+
tablo = Table(border_style="dim", show_lines=True, header_style="bold")
|
|
517
|
+
tablo.add_column("AI Aracı", min_width=12)
|
|
518
|
+
tablo.add_column("Dosya Sayısı", justify="right", min_width=13)
|
|
519
|
+
tablo.add_column("Ort. AI Skoru", justify="right", min_width=13)
|
|
520
|
+
tablo.add_column("Ort. Anlama", justify="right", min_width=12)
|
|
521
|
+
|
|
522
|
+
arac_emojileri = {
|
|
523
|
+
"copilot": "🐙", "cursor": "🖱️",
|
|
524
|
+
"claude": "🧠", "unknown": "❓",
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
for arac, veri in sorted(sonuclar.items(), key=lambda x: -x[1].get("dosya_sayisi", 0)):
|
|
528
|
+
emoji = arac_emojileri.get(arac, "🤖")
|
|
529
|
+
anlama = (
|
|
530
|
+
f"{veri['avg_understanding']:.1f}/5"
|
|
531
|
+
if veri.get("avg_understanding") else "—"
|
|
532
|
+
)
|
|
533
|
+
tablo.add_row(
|
|
534
|
+
f"{emoji} {arac}",
|
|
535
|
+
str(veri.get("dosya_sayisi", 0)),
|
|
536
|
+
f"%{veri.get('avg_ai_probability', 0) * 100:.0f}",
|
|
537
|
+
anlama,
|
|
538
|
+
)
|
|
539
|
+
|
|
540
|
+
console.print(tablo)
|
|
541
|
+
console.print()
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
# ---------------------------------------------------------------------------
|
|
545
|
+
# codedna onboarding
|
|
546
|
+
# ---------------------------------------------------------------------------
|
|
547
|
+
@app.command()
|
|
548
|
+
def onboarding(
|
|
549
|
+
author: Optional[str] = typer.Option(None, "--author", "-a", help="Tek yazar analizi"),
|
|
550
|
+
team: bool = typer.Option(False, "--team", "-t", help="Takım geneli özet"),
|
|
551
|
+
repo: Optional[Path] = typer.Option(None, "--repo", "-r", help="Git repo dizini"),
|
|
552
|
+
) -> None:
|
|
553
|
+
"""Geliştirici onboarding hızını ölç ve ramp-up eğrisini göster."""
|
|
554
|
+
from codedna.onboarding import (
|
|
555
|
+
get_author_curve, team_onboarding_summary, get_all_authors,
|
|
556
|
+
)
|
|
557
|
+
from codedna.plan import is_feature_available
|
|
558
|
+
|
|
559
|
+
if not is_feature_available("sprint_health"):
|
|
560
|
+
console.print(
|
|
561
|
+
Panel(
|
|
562
|
+
"[bold yellow]🔒 Bu özellik Team planında mevcut.[/bold yellow]\n"
|
|
563
|
+
"[dim]Yükseltmek için:[/dim] [cyan]codedna plan activate <LICENSE_KEY>[/cyan]",
|
|
564
|
+
border_style="yellow",
|
|
565
|
+
)
|
|
566
|
+
)
|
|
567
|
+
raise typer.Exit(1)
|
|
568
|
+
|
|
569
|
+
kok = repo or find_git_root() or Path.cwd()
|
|
570
|
+
db_yolu = _get_db(kok)
|
|
571
|
+
init_db(db_yolu)
|
|
572
|
+
|
|
573
|
+
console.print()
|
|
574
|
+
|
|
575
|
+
if team or not author:
|
|
576
|
+
# Takım özeti
|
|
577
|
+
console.print("[bold cyan]🧬 CodeDNA[/bold cyan] — Onboarding takım özeti\n")
|
|
578
|
+
|
|
579
|
+
with console.status("[dim]Analiz ediliyor...[/dim]"):
|
|
580
|
+
ozet = team_onboarding_summary(db_yolu)
|
|
581
|
+
|
|
582
|
+
if not ozet:
|
|
583
|
+
console.print("[yellow]Kayıtlı yazar bulunamadı.[/yellow]")
|
|
584
|
+
return
|
|
585
|
+
|
|
586
|
+
tablo = Table(
|
|
587
|
+
title="[bold cyan]🚀 Onboarding Özeti[/bold cyan]",
|
|
588
|
+
border_style="dim",
|
|
589
|
+
show_lines=True,
|
|
590
|
+
header_style="bold",
|
|
591
|
+
)
|
|
592
|
+
tablo.add_column("Yazar", min_width=18)
|
|
593
|
+
tablo.add_column("Commit", justify="right", min_width=8)
|
|
594
|
+
tablo.add_column("Anketli", justify="right", min_width=9)
|
|
595
|
+
tablo.add_column("Ramp-up", justify="center", min_width=12)
|
|
596
|
+
tablo.add_column("Son Anlama", justify="center", min_width=12)
|
|
597
|
+
|
|
598
|
+
for y in ozet:
|
|
599
|
+
ramp_str = (
|
|
600
|
+
f"{y['ramp_up_hafta']:.1f} hafta"
|
|
601
|
+
if y["ramp_up_hafta"] is not None
|
|
602
|
+
else ("[dim]Yeterli veri yok[/dim]" if not y["yeterli_veri"] else "[yellow]Eşik aşılmadı[/yellow]")
|
|
603
|
+
)
|
|
604
|
+
anlama_str = (
|
|
605
|
+
f"{y['son_ort_anlama']:.1f}/5" if y["son_ort_anlama"] else "—"
|
|
606
|
+
)
|
|
607
|
+
tablo.add_row(
|
|
608
|
+
y["yazar"],
|
|
609
|
+
str(y["toplam_commit"]),
|
|
610
|
+
str(y["anlama_skoru_olan"]),
|
|
611
|
+
ramp_str,
|
|
612
|
+
anlama_str,
|
|
613
|
+
)
|
|
614
|
+
|
|
615
|
+
console.print(tablo)
|
|
616
|
+
|
|
617
|
+
else:
|
|
618
|
+
# Tek yazar eğrisi
|
|
619
|
+
console.print(f"[bold cyan]🧬 CodeDNA[/bold cyan] — [bold]{author}[/bold] onboarding eğrisi\n")
|
|
620
|
+
|
|
621
|
+
with console.status("[dim]Analiz ediliyor...[/dim]"):
|
|
622
|
+
egri = get_author_curve(author, db_yolu)
|
|
623
|
+
|
|
624
|
+
if egri.toplam_commit == 0:
|
|
625
|
+
console.print(f"[yellow]'{author}' yazarına ait commit bulunamadı.[/yellow]")
|
|
626
|
+
return
|
|
627
|
+
|
|
628
|
+
# Ramp-up panel
|
|
629
|
+
ramp_str = (
|
|
630
|
+
f"[green]{egri.ramp_up_hafta:.1f} hafta[/green]"
|
|
631
|
+
if egri.ramp_up_hafta is not None
|
|
632
|
+
else "[yellow]Eşik henüz aşılmadı[/yellow]"
|
|
633
|
+
if egri.anlama_skoru_olan >= 5
|
|
634
|
+
else "[dim]Yeterli veri yok (en az 5 commit)[/dim]"
|
|
635
|
+
)
|
|
636
|
+
|
|
637
|
+
anlama_str = (
|
|
638
|
+
f"{egri.son_ort_anlama:.1f}/5" if egri.son_ort_anlama else "—"
|
|
639
|
+
)
|
|
640
|
+
|
|
641
|
+
console.print(
|
|
642
|
+
Panel(
|
|
643
|
+
f"[bold]Yazar:[/bold] {egri.yazar}\n"
|
|
644
|
+
f"[bold]Toplam commit:[/bold] {egri.toplam_commit}\n"
|
|
645
|
+
f"[bold]Anketli commit:[/bold] {egri.anlama_skoru_olan}\n"
|
|
646
|
+
f"[bold]Tahmini ramp-up:[/bold] {ramp_str}\n"
|
|
647
|
+
f"[bold]Son ort. anlama:[/bold] {anlama_str}",
|
|
648
|
+
title="[bold cyan]🚀 Onboarding Analizi[/bold cyan]",
|
|
649
|
+
border_style="cyan",
|
|
650
|
+
padding=(1, 2),
|
|
651
|
+
)
|
|
652
|
+
)
|
|
653
|
+
|
|
654
|
+
# Commit bazlı tablo (anket verisi olanlar)
|
|
655
|
+
anketli = [n for n in egri.noktalar if n.understanding_score is not None]
|
|
656
|
+
if anketli:
|
|
657
|
+
tablo = Table(border_style="dim", show_lines=True, header_style="bold")
|
|
658
|
+
tablo.add_column("#", justify="right", min_width=4)
|
|
659
|
+
tablo.add_column("Hash", style="dim", min_width=10)
|
|
660
|
+
tablo.add_column("Tarih", min_width=12)
|
|
661
|
+
tablo.add_column("Hafta", justify="right", min_width=7)
|
|
662
|
+
tablo.add_column("Anlama", justify="center", min_width=10)
|
|
663
|
+
|
|
664
|
+
for n in anketli:
|
|
665
|
+
skor = n.understanding_score or 0.0
|
|
666
|
+
renk = "green" if skor >= 4 else "yellow" if skor >= 2.5 else "red"
|
|
667
|
+
tablo.add_row(
|
|
668
|
+
str(n.commit_no),
|
|
669
|
+
n.commit_hash[:8],
|
|
670
|
+
n.tarih.strftime("%Y-%m-%d"),
|
|
671
|
+
str(n.hafta_no),
|
|
672
|
+
f"[{renk}]{skor:.1f}/5[/{renk}]",
|
|
673
|
+
)
|
|
674
|
+
console.print(tablo)
|
|
675
|
+
|
|
676
|
+
console.print()
|
|
677
|
+
|
|
678
|
+
|
|
679
|
+
# ---------------------------------------------------------------------------
|
|
680
|
+
# codedna pr-comment
|
|
681
|
+
# ---------------------------------------------------------------------------
|
|
682
|
+
@app.command(name="pr-comment")
|
|
683
|
+
def pr_comment(
|
|
684
|
+
repo: Optional[str] = typer.Option(None, "--repo", help="owner/repo formatında repo (boşsa GITHUB_REPOSITORY env'den alınır)"),
|
|
685
|
+
pr: Optional[int] = typer.Option(None, "--pr", help="PR numarası (boşsa event payload'dan algılanır)"),
|
|
686
|
+
rate: float = typer.Option(75.0, "--rate", help="Teknik borç saatlik ücreti"),
|
|
687
|
+
) -> None:
|
|
688
|
+
"""GitHub PR'ına CodeDNA analiz yorumu bırak (mevcut yorumu günceller, spam yapmaz)."""
|
|
689
|
+
import os
|
|
690
|
+
from codedna.integrations.github_bot import (
|
|
691
|
+
format_pr_comment, post_or_update_comment, github_actions_pr_bilgisi,
|
|
692
|
+
)
|
|
693
|
+
from codedna.tech_debt import calculate_repo_debt
|
|
694
|
+
|
|
695
|
+
# Token — asla loglanmaz
|
|
696
|
+
token = os.environ.get("GITHUB_TOKEN")
|
|
697
|
+
if not token:
|
|
698
|
+
console.print(
|
|
699
|
+
"[bold red]Hata:[/bold red] GITHUB_TOKEN ortam değişkeni tanımlanmamış."
|
|
700
|
+
)
|
|
701
|
+
raise typer.Exit(1)
|
|
702
|
+
|
|
703
|
+
# Repo ve PR numarası — önce parametre, sonra GitHub Actions env
|
|
704
|
+
hedef_repo = repo
|
|
705
|
+
hedef_pr = pr
|
|
706
|
+
|
|
707
|
+
if not hedef_repo or not hedef_pr:
|
|
708
|
+
bilgi = github_actions_pr_bilgisi()
|
|
709
|
+
if bilgi:
|
|
710
|
+
otomatik_repo, otomatik_pr = bilgi
|
|
711
|
+
hedef_repo = hedef_repo or otomatik_repo
|
|
712
|
+
hedef_pr = hedef_pr or otomatik_pr
|
|
713
|
+
|
|
714
|
+
if not hedef_repo or not hedef_pr:
|
|
715
|
+
console.print(
|
|
716
|
+
"[bold red]Hata:[/bold red] Repo ve PR numarası bulunamadı.\n"
|
|
717
|
+
"[dim]--repo owner/repo --pr 42 ile belirtin veya GitHub Actions içinde çalıştırın.[/dim]"
|
|
718
|
+
)
|
|
719
|
+
raise typer.Exit(1)
|
|
720
|
+
|
|
721
|
+
kok = find_git_root() or Path.cwd()
|
|
722
|
+
db_yolu = _get_db(kok)
|
|
723
|
+
init_db(db_yolu)
|
|
724
|
+
|
|
725
|
+
console.print(f"\n[bold cyan]🧬 CodeDNA[/bold cyan] — PR #{hedef_pr} analiz ediliyor...\n")
|
|
726
|
+
|
|
727
|
+
with console.status("[dim]Dosyalar taranıyor...[/dim]"):
|
|
728
|
+
from codedna.scorer import scan_repository
|
|
729
|
+
sonuclar = scan_repository(kok, max_files=200)
|
|
730
|
+
|
|
731
|
+
debt_ozeti_dict: Optional[dict] = None
|
|
732
|
+
try:
|
|
733
|
+
debt = calculate_repo_debt(kok, db_yolu, hourly_rate=rate)
|
|
734
|
+
debt_ozeti_dict = {
|
|
735
|
+
"toplam_debt_saatleri": debt.toplam_debt_saatleri,
|
|
736
|
+
"toplam_aylik_maliyet_usd": debt.toplam_aylik_maliyet_usd,
|
|
737
|
+
}
|
|
738
|
+
except Exception:
|
|
739
|
+
pass
|
|
740
|
+
|
|
741
|
+
yorum = format_pr_comment(sonuclar, debt_ozeti_dict)
|
|
742
|
+
|
|
743
|
+
try:
|
|
744
|
+
sonuc = post_or_update_comment(hedef_repo, hedef_pr, yorum, token)
|
|
745
|
+
yorum_url = sonuc.get("html_url", "")
|
|
746
|
+
console.print(
|
|
747
|
+
f"[green]✓[/green] PR yorumu gönderildi: [dim]{yorum_url}[/dim]\n"
|
|
748
|
+
)
|
|
749
|
+
except RuntimeError as e:
|
|
750
|
+
console.print(f"[bold red]Hata:[/bold red] GitHub API isteği başarısız: {e}")
|
|
751
|
+
raise typer.Exit(1)
|
|
752
|
+
|
|
753
|
+
|
|
754
|
+
# ---------------------------------------------------------------------------
|
|
755
|
+
# codedna protect
|
|
756
|
+
# ---------------------------------------------------------------------------
|
|
757
|
+
protect_app = typer.Typer(help="Korumalı modül yönetimi.")
|
|
758
|
+
app.add_typer(protect_app, name="protect")
|
|
759
|
+
|
|
760
|
+
|
|
761
|
+
@protect_app.command("add")
|
|
762
|
+
def protect_add(
|
|
763
|
+
file_path: str = typer.Argument(..., help="Korunacak dosya yolu"),
|
|
764
|
+
threshold: float = typer.Option(3.5, "--threshold", "-t", help="Minimum anlama skoru eşiği"),
|
|
765
|
+
label: str = typer.Option("", "--label", "-l", help="İnsan okunabilir etiket"),
|
|
766
|
+
repo: Optional[Path] = typer.Option(None, "--repo", "-r", help="Git repo dizini"),
|
|
767
|
+
) -> None:
|
|
768
|
+
"""Bir dosyayı korumalı modül olarak işaretle."""
|
|
769
|
+
from codedna.protection import protect_module
|
|
770
|
+
from codedna.plan import is_feature_available
|
|
771
|
+
|
|
772
|
+
if not is_feature_available("bus_factor"):
|
|
773
|
+
console.print("[bold yellow]🔒 Bu özellik Team planında mevcut.[/bold yellow]")
|
|
774
|
+
raise typer.Exit(1)
|
|
775
|
+
|
|
776
|
+
kok = repo or find_git_root() or Path.cwd()
|
|
777
|
+
db_yolu = _get_db(kok)
|
|
778
|
+
init_db(db_yolu)
|
|
779
|
+
|
|
780
|
+
# Göreli yolu tam yola çevir
|
|
781
|
+
tam_yol = str((kok / file_path).resolve())
|
|
782
|
+
etiket = label or file_path
|
|
783
|
+
yazar = "cli"
|
|
784
|
+
|
|
785
|
+
kayit_id = protect_module(tam_yol, threshold, etiket, yazar, db_yolu)
|
|
786
|
+
console.print(
|
|
787
|
+
f"[green]✓[/green] Korumalı modül eklendi: [cyan]{file_path}[/cyan]\n"
|
|
788
|
+
f" [dim]Etiket:[/dim] {etiket} · [dim]Eşik:[/dim] {threshold}/5 · [dim]ID:[/dim] #{kayit_id}"
|
|
789
|
+
)
|
|
790
|
+
|
|
791
|
+
|
|
792
|
+
@protect_app.command("remove")
|
|
793
|
+
def protect_remove(
|
|
794
|
+
file_path: str = typer.Argument(..., help="Koruma kaldırılacak dosya yolu"),
|
|
795
|
+
repo: Optional[Path] = typer.Option(None, "--repo", "-r", help="Git repo dizini"),
|
|
796
|
+
) -> None:
|
|
797
|
+
"""Bir dosyadan korumayı kaldır."""
|
|
798
|
+
from codedna.protection import unprotect_module
|
|
799
|
+
from codedna.plan import is_feature_available
|
|
800
|
+
|
|
801
|
+
if not is_feature_available("bus_factor"):
|
|
802
|
+
console.print("[bold yellow]🔒 Bu özellik Team planında mevcut.[/bold yellow]")
|
|
803
|
+
raise typer.Exit(1)
|
|
804
|
+
|
|
805
|
+
kok = repo or find_git_root() or Path.cwd()
|
|
806
|
+
db_yolu = _get_db(kok)
|
|
807
|
+
tam_yol = str((kok / file_path).resolve())
|
|
808
|
+
|
|
809
|
+
if unprotect_module(tam_yol, db_yolu):
|
|
810
|
+
console.print(f"[green]✓[/green] Koruma kaldırıldı: [cyan]{file_path}[/cyan]")
|
|
811
|
+
else:
|
|
812
|
+
console.print(f"[yellow]Bulunamadı:[/yellow] '{file_path}' korumalı modüller arasında yok.")
|
|
813
|
+
|
|
814
|
+
|
|
815
|
+
@protect_app.command("list")
|
|
816
|
+
def protect_list(
|
|
817
|
+
repo: Optional[Path] = typer.Option(None, "--repo", "-r", help="Git repo dizini"),
|
|
818
|
+
) -> None:
|
|
819
|
+
"""Tüm korumalı modülleri ve durumlarını göster."""
|
|
820
|
+
from codedna.protection import check_protected_modules
|
|
821
|
+
from codedna.plan import is_feature_available
|
|
822
|
+
|
|
823
|
+
if not is_feature_available("bus_factor"):
|
|
824
|
+
console.print("[bold yellow]🔒 Bu özellik Team planında mevcut.[/bold yellow]")
|
|
825
|
+
raise typer.Exit(1)
|
|
826
|
+
|
|
827
|
+
kok = repo or find_git_root() or Path.cwd()
|
|
828
|
+
db_yolu = _get_db(kok)
|
|
829
|
+
init_db(db_yolu)
|
|
830
|
+
|
|
831
|
+
moduller = check_protected_modules(db_yolu)
|
|
832
|
+
if not moduller:
|
|
833
|
+
console.print("[yellow]Henüz korumalı modül yok.[/yellow]")
|
|
834
|
+
console.print("[dim]Eklemek için:[/dim] [cyan]codedna protect add <dosya> --label 'Etiket'[/cyan]")
|
|
835
|
+
return
|
|
836
|
+
|
|
837
|
+
tablo = Table(
|
|
838
|
+
title="[bold cyan]🛡️ Korumalı Modüller[/bold cyan]",
|
|
839
|
+
border_style="dim", show_lines=True, header_style="bold",
|
|
840
|
+
)
|
|
841
|
+
tablo.add_column("Dosya", min_width=30)
|
|
842
|
+
tablo.add_column("Etiket", min_width=16)
|
|
843
|
+
tablo.add_column("Eşik", justify="center", min_width=7)
|
|
844
|
+
tablo.add_column("Mevcut", justify="center", min_width=9)
|
|
845
|
+
tablo.add_column("Durum", justify="center", min_width=12)
|
|
846
|
+
|
|
847
|
+
for m in moduller:
|
|
848
|
+
mevcut_str = f"{m.mevcut_skor:.1f}" if m.mevcut_skor is not None else "—"
|
|
849
|
+
if m.durum == "İHLAL":
|
|
850
|
+
durum_str = "[bold red]🔴 İHLAL[/bold red]"
|
|
851
|
+
elif m.durum == "GÜVENLİ":
|
|
852
|
+
durum_str = "[green]✅ GÜVENLİ[/green]"
|
|
853
|
+
else:
|
|
854
|
+
durum_str = "[dim]⚪ BİLİNMİYOR[/dim]"
|
|
855
|
+
|
|
856
|
+
try:
|
|
857
|
+
goreceli = str(Path(m.dosya_yolu).relative_to(kok))
|
|
858
|
+
except ValueError:
|
|
859
|
+
goreceli = m.dosya_yolu[-40:]
|
|
860
|
+
|
|
861
|
+
tablo.add_row(goreceli, m.etiket, f"{m.esik:.1f}", mevcut_str, durum_str)
|
|
862
|
+
|
|
863
|
+
console.print()
|
|
864
|
+
console.print(tablo)
|
|
865
|
+
console.print()
|
|
866
|
+
|
|
867
|
+
|
|
868
|
+
@protect_app.command("check")
|
|
869
|
+
def protect_check(
|
|
870
|
+
repo: Optional[Path] = typer.Option(None, "--repo", "-r", help="Git repo dizini"),
|
|
871
|
+
) -> None:
|
|
872
|
+
"""Sadece eşik altına düşmüş (ihlaldeki) modülleri göster."""
|
|
873
|
+
from codedna.protection import get_violations
|
|
874
|
+
from codedna.plan import is_feature_available
|
|
875
|
+
|
|
876
|
+
if not is_feature_available("bus_factor"):
|
|
877
|
+
console.print("[bold yellow]🔒 Bu özellik Team planında mevcut.[/bold yellow]")
|
|
878
|
+
raise typer.Exit(1)
|
|
879
|
+
|
|
880
|
+
kok = repo or find_git_root() or Path.cwd()
|
|
881
|
+
db_yolu = _get_db(kok)
|
|
882
|
+
init_db(db_yolu)
|
|
883
|
+
|
|
884
|
+
ihlaller = get_violations(db_yolu)
|
|
885
|
+
if not ihlaller:
|
|
886
|
+
console.print("[green]✅ Tüm korumalı modüller güvende.[/green]")
|
|
887
|
+
return
|
|
888
|
+
|
|
889
|
+
console.print()
|
|
890
|
+
for ihlal in ihlaller:
|
|
891
|
+
skor_str = f"{ihlal.mevcut_skor:.1f}" if ihlal.mevcut_skor else "?"
|
|
892
|
+
try:
|
|
893
|
+
goreceli = str(Path(ihlal.dosya_yolu).relative_to(kok))
|
|
894
|
+
except ValueError:
|
|
895
|
+
goreceli = ihlal.dosya_yolu
|
|
896
|
+
console.print(
|
|
897
|
+
f"[bold red]⚠️ İHLAL:[/bold red] [cyan]{goreceli}[/cyan] — "
|
|
898
|
+
f"anlama: [red]{skor_str}[/red] < eşik: [yellow]{ihlal.esik:.1f}[/yellow] "
|
|
899
|
+
f"([dim]{ihlal.etiket}[/dim])"
|
|
900
|
+
)
|
|
901
|
+
console.print()
|
|
902
|
+
|
|
903
|
+
|
|
904
|
+
# ---------------------------------------------------------------------------
|
|
905
|
+
# codedna interview
|
|
906
|
+
# ---------------------------------------------------------------------------
|
|
907
|
+
interview_app = typer.Typer(help="Aday mülakat aracı.")
|
|
908
|
+
app.add_typer(interview_app, name="interview")
|
|
909
|
+
|
|
910
|
+
|
|
911
|
+
@interview_app.command("start")
|
|
912
|
+
def interview_start(
|
|
913
|
+
candidate: str = typer.Option(..., "--candidate", "-c", help="Aday adı"),
|
|
914
|
+
difficulty: str = typer.Option("medium", "--difficulty", "-d", help="Zorluk: easy|medium|hard"),
|
|
915
|
+
repo: Optional[Path] = typer.Option(None, "--repo", "-r", help="Git repo dizini"),
|
|
916
|
+
) -> None:
|
|
917
|
+
"""Yeni mülakat oturumu başlat ve anonimleştirilmiş kod göster."""
|
|
918
|
+
from codedna.interview import select_candidate_file, generate_questions, start_session
|
|
919
|
+
from codedna.plan import is_feature_available
|
|
920
|
+
|
|
921
|
+
if not is_feature_available("interview_tool"):
|
|
922
|
+
console.print(
|
|
923
|
+
Panel(
|
|
924
|
+
"[bold yellow]🔒 Bu özellik Enterprise planında mevcut.[/bold yellow]\n"
|
|
925
|
+
"[dim]Yükseltmek için:[/dim] [cyan]codedna plan activate <LICENSE_KEY>[/cyan]",
|
|
926
|
+
border_style="yellow",
|
|
927
|
+
)
|
|
928
|
+
)
|
|
929
|
+
raise typer.Exit(1)
|
|
930
|
+
|
|
931
|
+
kok = repo or find_git_root() or Path.cwd()
|
|
932
|
+
db_yolu = _get_db(kok)
|
|
933
|
+
init_db(db_yolu)
|
|
934
|
+
|
|
935
|
+
console.print()
|
|
936
|
+
console.print(
|
|
937
|
+
"[dim]⚠️ Bu araç insan değerlendirmesinin YERİNE GEÇMEZ — tamamlayıcı bir sinyaldir.[/dim]\n"
|
|
938
|
+
)
|
|
939
|
+
|
|
940
|
+
with console.status("[dim]Uygun dosya seçiliyor...[/dim]"):
|
|
941
|
+
dosya = select_candidate_file(kok, db_yolu, difficulty)
|
|
942
|
+
|
|
943
|
+
if not dosya:
|
|
944
|
+
console.print(f"[yellow]'{difficulty}' zorluğunda uygun dosya bulunamadı.[/yellow]")
|
|
945
|
+
raise typer.Exit(1)
|
|
946
|
+
|
|
947
|
+
sorular = generate_questions(dosya.anonimlestirilmis_kod)
|
|
948
|
+
session_id = start_session(candidate, dosya.dosya_yolu, sorular, db_yolu)
|
|
949
|
+
|
|
950
|
+
console.print(
|
|
951
|
+
Panel(
|
|
952
|
+
f"[bold]Aday:[/bold] {candidate}\n"
|
|
953
|
+
f"[bold]Zorluk:[/bold] {difficulty} · [dim]Karmaşıklık:[/dim] {dosya.karmasiklik_skoru:.0f} · [dim]Satır:[/dim] {dosya.satir_sayisi}\n"
|
|
954
|
+
f"[bold]Oturum ID:[/bold] [cyan]#{session_id}[/cyan]",
|
|
955
|
+
title="[bold cyan]🎯 Mülakat Başladı[/bold cyan]",
|
|
956
|
+
border_style="cyan",
|
|
957
|
+
padding=(1, 2),
|
|
958
|
+
)
|
|
959
|
+
)
|
|
960
|
+
|
|
961
|
+
console.print("\n[bold]─── Anonimleştirilmiş Kod ───[/bold]")
|
|
962
|
+
console.print(f"[dim]{dosya.anonimlestirilmis_kod[:800]}[/dim]")
|
|
963
|
+
if len(dosya.anonimlestirilmis_kod) > 800:
|
|
964
|
+
console.print("[dim]... (kısaltıldı)[/dim]")
|
|
965
|
+
|
|
966
|
+
console.print("\n[bold]─── Sorular ───[/bold]")
|
|
967
|
+
for i, soru in enumerate(sorular, 1):
|
|
968
|
+
console.print(f" [bold cyan]{i}.[/bold cyan] {soru}")
|
|
969
|
+
|
|
970
|
+
console.print(
|
|
971
|
+
f"\n[dim]Değerlendirme için:[/dim] [cyan]codedna interview score {session_id} --score 4.0 --notes 'Not'[/cyan]\n"
|
|
972
|
+
)
|
|
973
|
+
|
|
974
|
+
|
|
975
|
+
@interview_app.command("list")
|
|
976
|
+
def interview_list(
|
|
977
|
+
repo: Optional[Path] = typer.Option(None, "--repo", "-r", help="Git repo dizini"),
|
|
978
|
+
limit: int = typer.Option(10, "--limit", "-n", help="Gösterilecek oturum sayısı"),
|
|
979
|
+
) -> None:
|
|
980
|
+
"""Geçmiş mülakat oturumlarını göster."""
|
|
981
|
+
from codedna.interview import get_sessions
|
|
982
|
+
from codedna.plan import is_feature_available
|
|
983
|
+
|
|
984
|
+
if not is_feature_available("interview_tool"):
|
|
985
|
+
console.print("[bold yellow]🔒 Bu özellik Enterprise planında mevcut.[/bold yellow]")
|
|
986
|
+
raise typer.Exit(1)
|
|
987
|
+
|
|
988
|
+
kok = repo or find_git_root() or Path.cwd()
|
|
989
|
+
db_yolu = _get_db(kok)
|
|
990
|
+
init_db(db_yolu)
|
|
991
|
+
|
|
992
|
+
oturumlar = get_sessions(db_yolu, limit=limit)
|
|
993
|
+
if not oturumlar:
|
|
994
|
+
console.print("[yellow]Henüz mülakat oturumu yok.[/yellow]")
|
|
995
|
+
return
|
|
996
|
+
|
|
997
|
+
tablo = Table(
|
|
998
|
+
title="[bold cyan]🎯 Mülakat Geçmişi[/bold cyan]",
|
|
999
|
+
border_style="dim", show_lines=True, header_style="bold",
|
|
1000
|
+
)
|
|
1001
|
+
tablo.add_column("#", justify="right", min_width=4)
|
|
1002
|
+
tablo.add_column("Aday", min_width=16)
|
|
1003
|
+
tablo.add_column("Başlangıç", min_width=17)
|
|
1004
|
+
tablo.add_column("Skor", justify="center", min_width=8)
|
|
1005
|
+
tablo.add_column("Notlar", min_width=20)
|
|
1006
|
+
|
|
1007
|
+
for o in oturumlar:
|
|
1008
|
+
skor_str = f"{o['skor']:.1f}/5" if o["skor"] is not None else "[dim]—[/dim]"
|
|
1009
|
+
tablo.add_row(
|
|
1010
|
+
str(o["id"]),
|
|
1011
|
+
o["aday"] or "?",
|
|
1012
|
+
o["baslangic"] or "?",
|
|
1013
|
+
skor_str,
|
|
1014
|
+
(o["notlar"] or "")[:30],
|
|
1015
|
+
)
|
|
1016
|
+
|
|
1017
|
+
console.print()
|
|
1018
|
+
console.print(tablo)
|
|
1019
|
+
console.print()
|
|
1020
|
+
|
|
1021
|
+
|
|
1022
|
+
@interview_app.command("score")
|
|
1023
|
+
def interview_score(
|
|
1024
|
+
session_id: int = typer.Argument(..., help="Oturum ID'si"),
|
|
1025
|
+
score: float = typer.Option(..., "--score", "-s", help="0.0–5.0 arası puan"),
|
|
1026
|
+
notes: str = typer.Option("", "--notes", "-n", help="Değerlendirici notları"),
|
|
1027
|
+
repo: Optional[Path] = typer.Option(None, "--repo", "-r", help="Git repo dizini"),
|
|
1028
|
+
) -> None:
|
|
1029
|
+
"""Mülakat oturumuna insan değerlendirmesi puanı ekle."""
|
|
1030
|
+
from codedna.interview import submit_score
|
|
1031
|
+
from codedna.plan import is_feature_available
|
|
1032
|
+
|
|
1033
|
+
if not is_feature_available("interview_tool"):
|
|
1034
|
+
console.print("[bold yellow]🔒 Bu özellik Enterprise planında mevcut.[/bold yellow]")
|
|
1035
|
+
raise typer.Exit(1)
|
|
1036
|
+
|
|
1037
|
+
kok = repo or find_git_root() or Path.cwd()
|
|
1038
|
+
db_yolu = _get_db(kok)
|
|
1039
|
+
|
|
1040
|
+
try:
|
|
1041
|
+
sonuc = submit_score(session_id, score, notes, db_yolu)
|
|
1042
|
+
console.print(
|
|
1043
|
+
f"[green]✓[/green] Değerlendirme kaydedildi: "
|
|
1044
|
+
f"Oturum [cyan]#{session_id}[/cyan] → [bold]{score:.1f}/5[/bold]"
|
|
1045
|
+
)
|
|
1046
|
+
if notes:
|
|
1047
|
+
console.print(f" [dim]Not:[/dim] {notes}")
|
|
1048
|
+
except ValueError as e:
|
|
1049
|
+
console.print(f"[bold red]Hata:[/bold red] {e}")
|
|
1050
|
+
raise typer.Exit(1)
|
|
1051
|
+
|
|
1052
|
+
|
|
1053
|
+
# ---------------------------------------------------------------------------
|
|
1054
|
+
# codedna bus-factor
|
|
1055
|
+
# ---------------------------------------------------------------------------
|
|
1056
|
+
@app.command(name="bus-factor")
|
|
1057
|
+
def bus_factor(
|
|
1058
|
+
repo: Optional[Path] = typer.Option(None, "--repo", "-r", help="Git repo dizini"),
|
|
1059
|
+
critical: bool = typer.Option(False, "--critical", "-c", help="Sadece kritik (bus_factor=1) dosyaları göster"),
|
|
1060
|
+
max_files: int = typer.Option(500, "--max", "-m", help="İşlenecek maksimum dosya sayısı"),
|
|
1061
|
+
) -> None:
|
|
1062
|
+
"""Repo genelinde bus factor analizi yap ve kritik sahiplik risklerini göster."""
|
|
1063
|
+
from codedna.bus_factor import calculate_bus_factor, get_at_risk_files, _BUYUK_REPO_ESIGI
|
|
1064
|
+
from codedna.plan import is_feature_available
|
|
1065
|
+
|
|
1066
|
+
# Plan kontrolü
|
|
1067
|
+
if not is_feature_available("bus_factor"):
|
|
1068
|
+
console.print(
|
|
1069
|
+
Panel(
|
|
1070
|
+
"[bold yellow]🔒 Bu özellik Team planında mevcut.[/bold yellow]\n"
|
|
1071
|
+
"[dim]This feature is available on Team plan.[/dim]\n\n"
|
|
1072
|
+
"[dim]Yükseltmek için:[/dim] [cyan]codedna plan activate <LICENSE_KEY>[/cyan]",
|
|
1073
|
+
border_style="yellow",
|
|
1074
|
+
padding=(1, 2),
|
|
1075
|
+
)
|
|
1076
|
+
)
|
|
1077
|
+
raise typer.Exit(1)
|
|
1078
|
+
|
|
1079
|
+
kok = repo or find_git_root() or Path.cwd()
|
|
1080
|
+
db_yolu = _get_db(kok)
|
|
1081
|
+
init_db(db_yolu)
|
|
1082
|
+
|
|
1083
|
+
if max_files > _BUYUK_REPO_ESIGI:
|
|
1084
|
+
console.print(
|
|
1085
|
+
f"[yellow]⚠[/yellow] {max_files} dosya taranacak — büyük repolar için yavaş olabilir."
|
|
1086
|
+
)
|
|
1087
|
+
|
|
1088
|
+
console.print()
|
|
1089
|
+
console.print("[bold cyan]🧬 CodeDNA[/bold cyan] — Bus Factor analizi çalışıyor...\n")
|
|
1090
|
+
|
|
1091
|
+
with console.status("[dim]git blame çalıştırılıyor...[/dim]"):
|
|
1092
|
+
if critical:
|
|
1093
|
+
sonuclar = get_at_risk_files(kok, db_yolu)
|
|
1094
|
+
else:
|
|
1095
|
+
sonuclar = calculate_bus_factor(kok, db_yolu, max_dosya=max_files)
|
|
1096
|
+
|
|
1097
|
+
if not sonuclar:
|
|
1098
|
+
console.print("[yellow]Analiz edilecek dosya bulunamadı.[/yellow]")
|
|
1099
|
+
return
|
|
1100
|
+
|
|
1101
|
+
# Tablo
|
|
1102
|
+
tablo = Table(
|
|
1103
|
+
title="",
|
|
1104
|
+
border_style="dim",
|
|
1105
|
+
show_lines=True,
|
|
1106
|
+
header_style="bold",
|
|
1107
|
+
)
|
|
1108
|
+
tablo.add_column("Dosya", style="white", min_width=30)
|
|
1109
|
+
tablo.add_column("Bus Factor", justify="center", min_width=11)
|
|
1110
|
+
tablo.add_column("Ana Sahip", min_width=16)
|
|
1111
|
+
tablo.add_column("Sahiplik %", justify="right", min_width=11)
|
|
1112
|
+
tablo.add_column("Risk", justify="center", min_width=10)
|
|
1113
|
+
|
|
1114
|
+
kritik_sayisi = 0
|
|
1115
|
+
for s in sonuclar:
|
|
1116
|
+
bf_str = str(s.bus_factor)
|
|
1117
|
+
if s.risk == "KRİTİK":
|
|
1118
|
+
bf_goster = f"[red]🚌 {bf_str}[/red]"
|
|
1119
|
+
risk_goster = "[bold red]KRİTİK[/bold red]"
|
|
1120
|
+
kritik_sayisi += 1
|
|
1121
|
+
elif s.risk == "RİSKLİ":
|
|
1122
|
+
bf_goster = f"[yellow]🚌 {bf_str}[/yellow]"
|
|
1123
|
+
risk_goster = "[yellow]RİSKLİ[/yellow]"
|
|
1124
|
+
else:
|
|
1125
|
+
bf_goster = f"[green]🚌 {bf_str}[/green]"
|
|
1126
|
+
risk_goster = "[green]GÜVENLİ[/green]"
|
|
1127
|
+
|
|
1128
|
+
tablo.add_row(
|
|
1129
|
+
s.dosya_yolu,
|
|
1130
|
+
bf_goster,
|
|
1131
|
+
s.birincil_sahip or "?",
|
|
1132
|
+
f"%{s.sahiplik_yuzdesi:.1f}",
|
|
1133
|
+
risk_goster,
|
|
1134
|
+
)
|
|
1135
|
+
|
|
1136
|
+
console.print(tablo)
|
|
1137
|
+
console.print(
|
|
1138
|
+
f"\n[bold]Özet:[/bold] {len(sonuclar)} dosya · "
|
|
1139
|
+
f"[red]{kritik_sayisi} kritik[/red] · "
|
|
1140
|
+
f"[dim]Eşik: anlama skoru ≥ 3.5[/dim]\n"
|
|
1141
|
+
)
|
|
1142
|
+
|
|
1143
|
+
|
|
1144
|
+
# ---------------------------------------------------------------------------
|
|
1145
|
+
# codedna debt
|
|
1146
|
+
# ---------------------------------------------------------------------------
|
|
1147
|
+
@app.command()
|
|
1148
|
+
def debt(
|
|
1149
|
+
repo: Optional[Path] = typer.Option(None, "--repo", "-r", help="Git repo dizini"),
|
|
1150
|
+
rate: float = typer.Option(75.0, "--rate", help="Saatlik maliyet ($/saat)"),
|
|
1151
|
+
file: Optional[Path] = typer.Option(None, "--file", "-f", help="Tek dosya analizi"),
|
|
1152
|
+
) -> None:
|
|
1153
|
+
"""Repo genelinde teknik borç maliyeti hesapla."""
|
|
1154
|
+
from codedna.tech_debt import calculate_repo_debt, calculate_file_debt
|
|
1155
|
+
from codedna.plan import get_current_plan, Plan
|
|
1156
|
+
|
|
1157
|
+
kok = repo or find_git_root() or Path.cwd()
|
|
1158
|
+
db_yolu = _get_db(kok)
|
|
1159
|
+
init_db(db_yolu)
|
|
1160
|
+
|
|
1161
|
+
mevcut_plan = get_current_plan()
|
|
1162
|
+
dolar_gizli = mevcut_plan == Plan.FREE # Free planda dolar tutarı gizli
|
|
1163
|
+
|
|
1164
|
+
console.print()
|
|
1165
|
+
console.print("[bold cyan]🧬 CodeDNA[/bold cyan] — Teknik borç hesaplanıyor...\n")
|
|
1166
|
+
|
|
1167
|
+
# Tek dosya modu
|
|
1168
|
+
if file:
|
|
1169
|
+
# Göreli yolu tam yola çevir
|
|
1170
|
+
tam_dosya = (kok / file).resolve() if not file.is_absolute() else file
|
|
1171
|
+
with console.status("[dim]Analiz ediliyor...[/dim]"):
|
|
1172
|
+
borc = calculate_file_debt(str(tam_dosya), db_yolu, hourly_rate=rate)
|
|
1173
|
+
|
|
1174
|
+
if not borc:
|
|
1175
|
+
console.print(f"[yellow]Uyarı:[/yellow] '{file}' için veri bulunamadı.")
|
|
1176
|
+
return
|
|
1177
|
+
|
|
1178
|
+
risk_renk = {"KRİTİK": "red", "YÜKSEK": "yellow", "ORTA": "yellow", "DÜŞÜK": "green"}.get(
|
|
1179
|
+
borc.risk_seviyesi, "white"
|
|
1180
|
+
)
|
|
1181
|
+
maliyet_str = (
|
|
1182
|
+
f"[dim]Pro+ plan gerekli[/dim]"
|
|
1183
|
+
if dolar_gizli
|
|
1184
|
+
else f"[bold green]${borc.aylik_maliyet_usd:.2f}/ay[/bold green]"
|
|
1185
|
+
)
|
|
1186
|
+
|
|
1187
|
+
console.print(
|
|
1188
|
+
Panel(
|
|
1189
|
+
f"[bold]Dosya:[/bold] [dim]{borc.dosya_yolu}[/dim]\n"
|
|
1190
|
+
f"[bold]Borç saati:[/bold] {borc.debt_saatleri:.1f} saat\n"
|
|
1191
|
+
f"[bold]Aylık maliyet:[/bold] {maliyet_str}\n"
|
|
1192
|
+
f"[bold]Risk:[/bold] [{risk_renk}]{borc.risk_seviyesi}[/{risk_renk}]\n"
|
|
1193
|
+
f"[bold]AI olasılığı:[/bold] %{borc.ai_olasiligi*100:.0f} · "
|
|
1194
|
+
f"[bold]Karmaşıklık:[/bold] {borc.karmasiklik:.0f} · "
|
|
1195
|
+
f"[bold]Satır:[/bold] {borc.toplam_satir}",
|
|
1196
|
+
title="[bold cyan]💰 Teknik Borç — Dosya Detayı[/bold cyan]",
|
|
1197
|
+
border_style="cyan",
|
|
1198
|
+
padding=(1, 2),
|
|
1199
|
+
)
|
|
1200
|
+
)
|
|
1201
|
+
return
|
|
1202
|
+
|
|
1203
|
+
# Repo genel modu
|
|
1204
|
+
with console.status("[dim]Tüm dosyalar analiz ediliyor...[/dim]"):
|
|
1205
|
+
ozet = calculate_repo_debt(kok, db_yolu, hourly_rate=rate)
|
|
1206
|
+
|
|
1207
|
+
maliyet_str = (
|
|
1208
|
+
"[dim]🔒 Pro+ plan gerekli[/dim]"
|
|
1209
|
+
if dolar_gizli
|
|
1210
|
+
else f"[bold green]${ozet.toplam_aylik_maliyet_usd:.2f}/ay[/bold green]"
|
|
1211
|
+
)
|
|
1212
|
+
|
|
1213
|
+
console.print(
|
|
1214
|
+
Panel(
|
|
1215
|
+
f"[bold]💰 Teknik Borç Özeti[/bold]\n\n"
|
|
1216
|
+
f"[bold]Toplam tahmini borç:[/bold] [cyan]{ozet.toplam_debt_saatleri:.1f} saat[/cyan]\n"
|
|
1217
|
+
f"[bold]Aylık maliyet:[/bold] {maliyet_str}\n"
|
|
1218
|
+
f"[bold]Saatlik ücret:[/bold] [dim]${rate:.0f}/saat[/dim]\n"
|
|
1219
|
+
f"[bold]Analiz edilen dosya:[/bold] {ozet.toplam_dosya}",
|
|
1220
|
+
border_style="cyan",
|
|
1221
|
+
padding=(1, 2),
|
|
1222
|
+
)
|
|
1223
|
+
)
|
|
1224
|
+
|
|
1225
|
+
if ozet.en_pahali_5:
|
|
1226
|
+
console.print("[bold]En maliyetli 5 dosya:[/bold]")
|
|
1227
|
+
tablo = Table(border_style="dim", show_lines=True, header_style="bold")
|
|
1228
|
+
tablo.add_column("Dosya", style="white", min_width=30)
|
|
1229
|
+
tablo.add_column("Borç Saati", justify="right", min_width=11)
|
|
1230
|
+
tablo.add_column("Aylık", justify="right", min_width=10)
|
|
1231
|
+
tablo.add_column("Risk", justify="center", min_width=10)
|
|
1232
|
+
|
|
1233
|
+
for d in ozet.en_pahali_5:
|
|
1234
|
+
risk_renk = {
|
|
1235
|
+
"KRİTİK": "red", "YÜKSEK": "yellow",
|
|
1236
|
+
"ORTA": "yellow", "DÜŞÜK": "green",
|
|
1237
|
+
}.get(d.risk_seviyesi, "white")
|
|
1238
|
+
|
|
1239
|
+
aylik = (
|
|
1240
|
+
"[dim]gizli[/dim]"
|
|
1241
|
+
if dolar_gizli
|
|
1242
|
+
else f"[{risk_renk}]${d.aylik_maliyet_usd:.2f}[/{risk_renk}]"
|
|
1243
|
+
)
|
|
1244
|
+
|
|
1245
|
+
# Dosya yolunu kısalt
|
|
1246
|
+
try:
|
|
1247
|
+
goreceli = str(Path(d.dosya_yolu).relative_to(kok))
|
|
1248
|
+
except ValueError:
|
|
1249
|
+
goreceli = d.dosya_yolu[-40:]
|
|
1250
|
+
|
|
1251
|
+
tablo.add_row(
|
|
1252
|
+
goreceli,
|
|
1253
|
+
f"{d.debt_saatleri:.1f} saat",
|
|
1254
|
+
aylik,
|
|
1255
|
+
f"[{risk_renk}]{d.risk_seviyesi}[/{risk_renk}]",
|
|
1256
|
+
)
|
|
1257
|
+
console.print(tablo)
|
|
1258
|
+
|
|
1259
|
+
if dolar_gizli:
|
|
1260
|
+
console.print(
|
|
1261
|
+
"\n[dim]💡 Dolar tutarları Pro+ planda görünür: [cyan]codedna plan activate <KEY>[/cyan][/dim]\n"
|
|
1262
|
+
)
|
|
1263
|
+
else:
|
|
1264
|
+
console.print()
|
|
1265
|
+
|
|
1266
|
+
|
|
1267
|
+
# ---------------------------------------------------------------------------
|
|
1268
|
+
# codedna sprint
|
|
1269
|
+
# ---------------------------------------------------------------------------
|
|
1270
|
+
sprint_app = typer.Typer(help="Sprint yönetimi ve sağlık skoru.")
|
|
1271
|
+
app.add_typer(sprint_app, name="sprint")
|
|
1272
|
+
|
|
1273
|
+
|
|
1274
|
+
@sprint_app.command("create")
|
|
1275
|
+
def sprint_olustur(
|
|
1276
|
+
name: str = typer.Option(..., "--name", "-n", help="Sprint ismi"),
|
|
1277
|
+
start: str = typer.Option(..., "--start", "-s", help="Başlangıç tarihi (YYYY-MM-DD)"),
|
|
1278
|
+
end: str = typer.Option(..., "--end", "-e", help="Bitiş tarihi (YYYY-MM-DD)"),
|
|
1279
|
+
repo: Optional[Path] = typer.Option(None, "--repo", "-r", help="Git repo dizini"),
|
|
1280
|
+
) -> None:
|
|
1281
|
+
"""Yeni sprint oluştur ve sağlık skoru hesapla."""
|
|
1282
|
+
from datetime import datetime as dt
|
|
1283
|
+
from codedna.sprint_health import calculate_sprint_health, save_sprint_result
|
|
1284
|
+
from codedna.plan import is_feature_available
|
|
1285
|
+
|
|
1286
|
+
if not is_feature_available("sprint_health"):
|
|
1287
|
+
console.print(
|
|
1288
|
+
Panel(
|
|
1289
|
+
"[bold yellow]🔒 Bu özellik Team planında mevcut.[/bold yellow]\n"
|
|
1290
|
+
"[dim]This feature is available on Team plan.[/dim]\n\n"
|
|
1291
|
+
"[dim]Yükseltmek için:[/dim] [cyan]codedna plan activate <LICENSE_KEY>[/cyan]",
|
|
1292
|
+
border_style="yellow",
|
|
1293
|
+
padding=(1, 2),
|
|
1294
|
+
)
|
|
1295
|
+
)
|
|
1296
|
+
raise typer.Exit(1)
|
|
1297
|
+
|
|
1298
|
+
# Tarihleri parse et
|
|
1299
|
+
try:
|
|
1300
|
+
baslangic = dt.fromisoformat(start)
|
|
1301
|
+
bitis = dt.fromisoformat(end)
|
|
1302
|
+
except ValueError:
|
|
1303
|
+
console.print(f"[bold red]Hata:[/bold red] Geçersiz tarih formatı. Kullanım: YYYY-MM-DD")
|
|
1304
|
+
raise typer.Exit(1)
|
|
1305
|
+
|
|
1306
|
+
if bitis <= baslangic:
|
|
1307
|
+
console.print("[bold red]Hata:[/bold red] Bitiş tarihi başlangıçtan sonra olmalı.")
|
|
1308
|
+
raise typer.Exit(1)
|
|
1309
|
+
|
|
1310
|
+
kok = repo or find_git_root() or Path.cwd()
|
|
1311
|
+
db_yolu = _get_db(kok)
|
|
1312
|
+
init_db(db_yolu)
|
|
1313
|
+
|
|
1314
|
+
console.print()
|
|
1315
|
+
console.print(f"[bold cyan]🧬 CodeDNA[/bold cyan] — [bold]{name}[/bold] sprint analizi çalışıyor...\n")
|
|
1316
|
+
|
|
1317
|
+
with console.status("[dim]Commit'ler analiz ediliyor...[/dim]"):
|
|
1318
|
+
sonuc = calculate_sprint_health(kok, db_yolu, baslangic, bitis, name)
|
|
1319
|
+
|
|
1320
|
+
sprint_id = save_sprint_result(sonuc, db_yolu)
|
|
1321
|
+
|
|
1322
|
+
durum_renk = {
|
|
1323
|
+
"SAĞLIKLI": "green", "DİKKAT": "yellow", "RİSKLİ": "red"
|
|
1324
|
+
}.get(sonuc.durum, "white")
|
|
1325
|
+
|
|
1326
|
+
console.print(
|
|
1327
|
+
Panel(
|
|
1328
|
+
f"[bold]Sprint:[/bold] {sonuc.sprint_adi}\n"
|
|
1329
|
+
f"[bold]Tarih:[/bold] {start} → {end}\n"
|
|
1330
|
+
f"[bold]Sağlık Skoru:[/bold] [{durum_renk}]{sonuc.health_score:.1f}/100 ({sonuc.durum})[/{durum_renk}]\n"
|
|
1331
|
+
f"[bold]Toplam Commit:[/bold] {sonuc.toplam_commit}\n"
|
|
1332
|
+
f"[bold]Ort. Anlama:[/bold] {f'{sonuc.avg_understanding:.1f}/5' if sonuc.avg_understanding else 'Veri yok'}\n"
|
|
1333
|
+
f"[bold]AI Oranı:[/bold] %{sonuc.ai_orani * 100:.0f} yüksek riskli\n"
|
|
1334
|
+
f"[bold]Borç Delta:[/bold] {sonuc.debt_delta_saati:.1f} saat/commit",
|
|
1335
|
+
title=f"[bold cyan]🏃 Sprint Sağlık Raporu — #{sprint_id}[/bold cyan]",
|
|
1336
|
+
border_style=durum_renk,
|
|
1337
|
+
padding=(1, 2),
|
|
1338
|
+
)
|
|
1339
|
+
)
|
|
1340
|
+
console.print(f"[dim]Sprint #{sprint_id} kaydedildi.[/dim]\n")
|
|
1341
|
+
|
|
1342
|
+
|
|
1343
|
+
@sprint_app.command("health")
|
|
1344
|
+
def sprint_sagligi(
|
|
1345
|
+
repo: Optional[Path] = typer.Option(None, "--repo", "-r", help="Git repo dizini"),
|
|
1346
|
+
) -> None:
|
|
1347
|
+
"""Son sprint'in sağlık skorunu göster."""
|
|
1348
|
+
from codedna.db import get_latest_sprint
|
|
1349
|
+
from codedna.plan import is_feature_available
|
|
1350
|
+
|
|
1351
|
+
if not is_feature_available("sprint_health"):
|
|
1352
|
+
console.print(
|
|
1353
|
+
Panel(
|
|
1354
|
+
"[bold yellow]🔒 Bu özellik Team planında mevcut.[/bold yellow]\n"
|
|
1355
|
+
"[dim]Yükseltmek için:[/dim] [cyan]codedna plan activate <LICENSE_KEY>[/cyan]",
|
|
1356
|
+
border_style="yellow",
|
|
1357
|
+
)
|
|
1358
|
+
)
|
|
1359
|
+
raise typer.Exit(1)
|
|
1360
|
+
|
|
1361
|
+
kok = repo or find_git_root() or Path.cwd()
|
|
1362
|
+
db_yolu = _get_db(kok)
|
|
1363
|
+
init_db(db_yolu)
|
|
1364
|
+
|
|
1365
|
+
sprint = get_latest_sprint(db_path=db_yolu)
|
|
1366
|
+
if not sprint:
|
|
1367
|
+
console.print("[yellow]Henüz kayıtlı sprint yok.[/yellow]")
|
|
1368
|
+
console.print("[dim]Oluşturmak için:[/dim] [cyan]codedna sprint create --name 'Sprint 1' --start 2026-06-01 --end 2026-06-14[/cyan]")
|
|
1369
|
+
return
|
|
1370
|
+
|
|
1371
|
+
skor = sprint["health_score"] or 0.0
|
|
1372
|
+
durum = "SAĞLIKLI" if skor >= 80 else "DİKKAT" if skor >= 50 else "RİSKLİ"
|
|
1373
|
+
durum_renk = {"SAĞLIKLI": "green", "DİKKAT": "yellow", "RİSKLİ": "red"}[durum]
|
|
1374
|
+
|
|
1375
|
+
from datetime import datetime as dt
|
|
1376
|
+
bas = dt.fromtimestamp(sprint["start_date"]).strftime("%Y-%m-%d") if sprint["start_date"] else "?"
|
|
1377
|
+
bit = dt.fromtimestamp(sprint["end_date"]).strftime("%Y-%m-%d") if sprint["end_date"] else "?"
|
|
1378
|
+
|
|
1379
|
+
anlama_val = sprint["avg_understanding"]
|
|
1380
|
+
anlama_str = f"{anlama_val:.1f}/5" if anlama_val else "Veri yok"
|
|
1381
|
+
delta_val = sprint["debt_delta_hours"] or 0.0
|
|
1382
|
+
|
|
1383
|
+
console.print()
|
|
1384
|
+
console.print(
|
|
1385
|
+
Panel(
|
|
1386
|
+
f"[bold]Sprint:[/bold] {sprint['sprint_name']}\n"
|
|
1387
|
+
f"[bold]Tarih:[/bold] {bas} → {bit}\n"
|
|
1388
|
+
f"[bold]Sağlık Skoru:[/bold] [{durum_renk}]{skor:.1f}/100 ({durum})[/{durum_renk}]\n"
|
|
1389
|
+
f"[bold]Ort. Anlama:[/bold] {anlama_str}\n"
|
|
1390
|
+
f"[bold]Borç Delta:[/bold] {delta_val:.1f} saat/commit",
|
|
1391
|
+
title="[bold cyan]🏃 Son Sprint Sağlığı[/bold cyan]",
|
|
1392
|
+
border_style=durum_renk,
|
|
1393
|
+
padding=(1, 2),
|
|
1394
|
+
)
|
|
1395
|
+
)
|
|
1396
|
+
|
|
1397
|
+
|
|
1398
|
+
@sprint_app.command("history")
|
|
1399
|
+
def sprint_gecmisi(
|
|
1400
|
+
repo: Optional[Path] = typer.Option(None, "--repo", "-r", help="Git repo dizini"),
|
|
1401
|
+
limit: int = typer.Option(10, "--limit", "-n", help="Gösterilecek sprint sayısı"),
|
|
1402
|
+
) -> None:
|
|
1403
|
+
"""Geçmiş sprint'leri tablo olarak göster."""
|
|
1404
|
+
from codedna.db import get_sprint_history as db_sprint_gecmisi
|
|
1405
|
+
from codedna.plan import is_feature_available
|
|
1406
|
+
from datetime import datetime as dt
|
|
1407
|
+
|
|
1408
|
+
if not is_feature_available("sprint_health"):
|
|
1409
|
+
console.print("[bold yellow]🔒 Bu özellik Team planında mevcut.[/bold yellow]")
|
|
1410
|
+
raise typer.Exit(1)
|
|
1411
|
+
|
|
1412
|
+
kok = repo or find_git_root() or Path.cwd()
|
|
1413
|
+
db_yolu = _get_db(kok)
|
|
1414
|
+
init_db(db_yolu)
|
|
1415
|
+
|
|
1416
|
+
sprintler = db_sprint_gecmisi(limit=limit, db_path=db_yolu)
|
|
1417
|
+
if not sprintler:
|
|
1418
|
+
console.print("[yellow]Henüz kayıtlı sprint yok.[/yellow]")
|
|
1419
|
+
return
|
|
1420
|
+
|
|
1421
|
+
tablo = Table(
|
|
1422
|
+
title=f"[bold cyan]🏃 Sprint Geçmişi — Son {len(sprintler)}[/bold cyan]",
|
|
1423
|
+
border_style="dim",
|
|
1424
|
+
show_lines=True,
|
|
1425
|
+
header_style="bold",
|
|
1426
|
+
)
|
|
1427
|
+
tablo.add_column("Sprint", min_width=16)
|
|
1428
|
+
tablo.add_column("Tarih Aralığı", min_width=22)
|
|
1429
|
+
tablo.add_column("Sağlık", justify="center", min_width=14)
|
|
1430
|
+
tablo.add_column("Anlama", justify="center", min_width=10)
|
|
1431
|
+
tablo.add_column("Borç Delta", justify="right", min_width=11)
|
|
1432
|
+
|
|
1433
|
+
for s in sprintler:
|
|
1434
|
+
skor = s["health_score"] or 0.0
|
|
1435
|
+
durum = "SAĞLIKLI" if skor >= 80 else "DİKKAT" if skor >= 50 else "RİSKLİ"
|
|
1436
|
+
renk = {"SAĞLIKLI": "green", "DİKKAT": "yellow", "RİSKLİ": "red"}[durum]
|
|
1437
|
+
|
|
1438
|
+
bas = dt.fromtimestamp(s["start_date"]).strftime("%Y-%m-%d") if s["start_date"] else "?"
|
|
1439
|
+
bit = dt.fromtimestamp(s["end_date"]).strftime("%Y-%m-%d") if s["end_date"] else "?"
|
|
1440
|
+
|
|
1441
|
+
anlama_str = f"{s['avg_understanding']:.1f}/5" if s["avg_understanding"] else "—"
|
|
1442
|
+
delta_str = f"{s['debt_delta_hours']:.1f}s" if s["debt_delta_hours"] else "—"
|
|
1443
|
+
|
|
1444
|
+
tablo.add_row(
|
|
1445
|
+
s["sprint_name"] or "?",
|
|
1446
|
+
f"{bas} → {bit}",
|
|
1447
|
+
f"[{renk}]{skor:.0f}/100 {durum}[/{renk}]",
|
|
1448
|
+
anlama_str,
|
|
1449
|
+
delta_str,
|
|
1450
|
+
)
|
|
1451
|
+
|
|
1452
|
+
console.print()
|
|
1453
|
+
console.print(tablo)
|
|
1454
|
+
console.print()
|
|
1455
|
+
|
|
1456
|
+
|
|
1457
|
+
# ---------------------------------------------------------------------------
|
|
1458
|
+
# codedna plan
|
|
1459
|
+
# ---------------------------------------------------------------------------
|
|
1460
|
+
@app.command()
|
|
1461
|
+
def plan(
|
|
1462
|
+
komut: Optional[str] = typer.Argument(
|
|
1463
|
+
None,
|
|
1464
|
+
help="'activate', 'demo' veya doğrudan lisans anahtarı",
|
|
1465
|
+
),
|
|
1466
|
+
anahtar: Optional[str] = typer.Argument(
|
|
1467
|
+
None,
|
|
1468
|
+
help="Lisans anahtarı (activate/demo ile birlikte kullanılır)",
|
|
1469
|
+
),
|
|
1470
|
+
) -> None:
|
|
1471
|
+
"""Mevcut planı göster, demo lisans aktif et veya lisans anahtarı ile plan aktif et.
|
|
1472
|
+
|
|
1473
|
+
Kullanım:
|
|
1474
|
+
codedna plan # mevcut planı göster
|
|
1475
|
+
codedna plan activate <KEY> # lisans aktif et (CDNA-PRO/TEAM/ENT-...)
|
|
1476
|
+
codedna plan demo # Enterprise demo lisans aktif et (hızlı test)
|
|
1477
|
+
codedna plan demo pro # Pro demo lisans aktif et
|
|
1478
|
+
codedna plan <KEY> # kısa yol
|
|
1479
|
+
"""
|
|
1480
|
+
from codedna.plan import (
|
|
1481
|
+
Plan as PlanEnum,
|
|
1482
|
+
get_current_plan,
|
|
1483
|
+
activate_license,
|
|
1484
|
+
activate_demo_license,
|
|
1485
|
+
get_plan_limits,
|
|
1486
|
+
)
|
|
1487
|
+
|
|
1488
|
+
# "demo [plan]" syntax'ı
|
|
1489
|
+
if komut == "demo":
|
|
1490
|
+
demo_plan_str = (anahtar or "enterprise").lower()
|
|
1491
|
+
try:
|
|
1492
|
+
demo_plan = PlanEnum(demo_plan_str)
|
|
1493
|
+
except ValueError:
|
|
1494
|
+
console.print(f"[bold red]Hata:[/bold red] Geçersiz plan: {demo_plan_str!r}")
|
|
1495
|
+
console.print("Geçerli planlar: free, pro, team, enterprise")
|
|
1496
|
+
raise typer.Exit(1)
|
|
1497
|
+
aktif_plan = activate_demo_license(demo_plan)
|
|
1498
|
+
console.print(
|
|
1499
|
+
Panel(
|
|
1500
|
+
f"[bold green]✓ Demo lisans aktif edildi![/bold green]\n\n"
|
|
1501
|
+
f"[bold]Plan:[/bold] [cyan]{aktif_plan.value.upper()}[/cyan]\n"
|
|
1502
|
+
f"[bold yellow]⚠️ Bu bir DEMO lisanstır — production için gerçek key gerekir.[/bold yellow]\n\n"
|
|
1503
|
+
f"Gerçek lisans için:\n"
|
|
1504
|
+
f"[dim]codedna plan activate CDNA-{demo_plan_str.upper()[:3]}-XXXX-XXXX-XXXX[/dim]",
|
|
1505
|
+
title="[bold cyan]🧬 CodeDNA — Demo Plan[/bold cyan]",
|
|
1506
|
+
border_style="cyan",
|
|
1507
|
+
padding=(1, 2),
|
|
1508
|
+
)
|
|
1509
|
+
)
|
|
1510
|
+
return
|
|
1511
|
+
|
|
1512
|
+
# "activate <KEY>" veya doğrudan "<KEY>" syntax'ı destekle
|
|
1513
|
+
lisans_anahtari: Optional[str] = None
|
|
1514
|
+
if komut == "activate" and anahtar:
|
|
1515
|
+
lisans_anahtari = anahtar
|
|
1516
|
+
elif komut and komut != "activate":
|
|
1517
|
+
lisans_anahtari = komut
|
|
1518
|
+
|
|
1519
|
+
if lisans_anahtari:
|
|
1520
|
+
# Lisans aktifleştirme
|
|
1521
|
+
try:
|
|
1522
|
+
aktif_plan = activate_license(lisans_anahtari)
|
|
1523
|
+
console.print(
|
|
1524
|
+
Panel(
|
|
1525
|
+
f"[bold green]✓ Lisans aktif edildi![/bold green]\n\n"
|
|
1526
|
+
f"[bold]Plan:[/bold] [cyan]{aktif_plan.value.upper()}[/cyan]\n"
|
|
1527
|
+
f"[bold]Anahtar:[/bold] [dim]{lisans_anahtari[:12]}...[/dim]",
|
|
1528
|
+
border_style="green",
|
|
1529
|
+
padding=(1, 2),
|
|
1530
|
+
)
|
|
1531
|
+
)
|
|
1532
|
+
except ValueError as e:
|
|
1533
|
+
console.print(f"[bold red]Hata:[/bold red] {e}")
|
|
1534
|
+
raise typer.Exit(1)
|
|
1535
|
+
return
|
|
1536
|
+
|
|
1537
|
+
# Mevcut planı göster
|
|
1538
|
+
mevcut = get_current_plan()
|
|
1539
|
+
limitler = get_plan_limits()
|
|
1540
|
+
|
|
1541
|
+
plan_renk = {
|
|
1542
|
+
PlanEnum.FREE: "dim",
|
|
1543
|
+
PlanEnum.PRO: "cyan",
|
|
1544
|
+
PlanEnum.TEAM: "green",
|
|
1545
|
+
PlanEnum.ENTERPRISE: "yellow",
|
|
1546
|
+
}.get(mevcut, "white")
|
|
1547
|
+
|
|
1548
|
+
tablo = Table(border_style="dim", show_header=False, padding=(0, 1))
|
|
1549
|
+
tablo.add_column("Özellik", style="dim")
|
|
1550
|
+
tablo.add_column("Değer", style="white")
|
|
1551
|
+
|
|
1552
|
+
for k, v in limitler.items():
|
|
1553
|
+
if isinstance(v, bool):
|
|
1554
|
+
goster = "[green]✓[/green]" if v else "[red]✗[/red]"
|
|
1555
|
+
elif isinstance(v, int) and v == -1:
|
|
1556
|
+
goster = "[dim]Sınırsız[/dim]"
|
|
1557
|
+
else:
|
|
1558
|
+
goster = str(v)
|
|
1559
|
+
tablo.add_row(k.replace("_", " ").title(), goster)
|
|
1560
|
+
|
|
1561
|
+
console.print()
|
|
1562
|
+
console.print(
|
|
1563
|
+
Panel(
|
|
1564
|
+
f"[bold]Mevcut Plan / Current Plan:[/bold] [{plan_renk}]{mevcut.value.upper()}[/{plan_renk}]\n\n"
|
|
1565
|
+
+ (
|
|
1566
|
+
"[dim]Lisans aktif etmek için:[/dim] [cyan]codedna plan activate <LICENSE_KEY>[/cyan]"
|
|
1567
|
+
if mevcut == PlanEnum.FREE
|
|
1568
|
+
else "[dim]Yükseltmek için:[/dim] [cyan]codedna plan activate <LICENSE_KEY>[/cyan]"
|
|
1569
|
+
),
|
|
1570
|
+
title="[bold cyan]🧬 CodeDNA Plan[/bold cyan]",
|
|
1571
|
+
border_style="cyan",
|
|
1572
|
+
padding=(1, 2),
|
|
1573
|
+
)
|
|
1574
|
+
)
|
|
1575
|
+
console.print(tablo)
|
|
1576
|
+
console.print()
|
|
1577
|
+
|
|
1578
|
+
|
|
1579
|
+
# ---------------------------------------------------------------------------
|
|
1580
|
+
# codedna dashboard
|
|
1581
|
+
# ---------------------------------------------------------------------------
|
|
1582
|
+
@app.command()
|
|
1583
|
+
def dashboard(
|
|
1584
|
+
api_port: int = typer.Option(8000, "--api-port", help="FastAPI port numarası"),
|
|
1585
|
+
ui_port: int = typer.Option(3000, "--ui-port", help="Next.js dashboard port numarası"),
|
|
1586
|
+
repo: Optional[Path] = typer.Option(None, "--repo", "-r", help="Git repo dizini"),
|
|
1587
|
+
) -> None:
|
|
1588
|
+
"""FastAPI + Next.js dashboard'u başlat ve tarayıcıda aç."""
|
|
1589
|
+
import os
|
|
1590
|
+
import subprocess
|
|
1591
|
+
import time
|
|
1592
|
+
import webbrowser
|
|
1593
|
+
|
|
1594
|
+
kok = repo or find_git_root() or Path.cwd()
|
|
1595
|
+
db_yolu = _get_db(kok)
|
|
1596
|
+
init_db(db_yolu)
|
|
1597
|
+
|
|
1598
|
+
# dashboard/ klasörünü bul — CLI'nin bulunduğu yere göre veya CWD'ye göre
|
|
1599
|
+
dashboard_yollari = [
|
|
1600
|
+
Path(__file__).parent.parent / "dashboard",
|
|
1601
|
+
Path.cwd() / "dashboard",
|
|
1602
|
+
kok / "dashboard",
|
|
1603
|
+
]
|
|
1604
|
+
dashboard_kok = next((p for p in dashboard_yollari if (p / "package.json").exists()), None)
|
|
1605
|
+
|
|
1606
|
+
if not dashboard_kok:
|
|
1607
|
+
console.print(
|
|
1608
|
+
"[bold red]Hata:[/bold red] dashboard/ klasörü bulunamadı.\n"
|
|
1609
|
+
"[dim]codedna proje kökünde 'dashboard/' klasörü olmalı.[/dim]"
|
|
1610
|
+
)
|
|
1611
|
+
raise typer.Exit(1)
|
|
1612
|
+
|
|
1613
|
+
console.print()
|
|
1614
|
+
console.print(
|
|
1615
|
+
Panel(
|
|
1616
|
+
f"[bold cyan]🧬 CodeDNA Panosu[/bold cyan] başlatılıyor...\n\n"
|
|
1617
|
+
f"[bold]API:[/bold] [link]http://localhost:{api_port}[/link]\n"
|
|
1618
|
+
f"[bold]Dashboard:[/bold] [link]http://localhost:{ui_port}[/link]\n\n"
|
|
1619
|
+
"[dim]Durdurmak için Ctrl+C[/dim]",
|
|
1620
|
+
border_style="cyan",
|
|
1621
|
+
padding=(1, 2),
|
|
1622
|
+
)
|
|
1623
|
+
)
|
|
1624
|
+
|
|
1625
|
+
# Ortam değişkenlerini ayarla
|
|
1626
|
+
env = os.environ.copy()
|
|
1627
|
+
env["CODEDNA_REPO_PATH"] = str(kok)
|
|
1628
|
+
env["CODEDNA_DB_PATH"] = str(db_yolu)
|
|
1629
|
+
env["NEXT_PUBLIC_API_URL"] = f"http://localhost:{api_port}"
|
|
1630
|
+
|
|
1631
|
+
# FastAPI sürecini başlat — venv Python'unu kullan
|
|
1632
|
+
import sys
|
|
1633
|
+
python_bin = sys.executable
|
|
1634
|
+
|
|
1635
|
+
api_proc = subprocess.Popen(
|
|
1636
|
+
[
|
|
1637
|
+
python_bin, "-m", "uvicorn",
|
|
1638
|
+
"codedna.api:app",
|
|
1639
|
+
"--host", "127.0.0.1",
|
|
1640
|
+
"--port", str(api_port),
|
|
1641
|
+
],
|
|
1642
|
+
env=env,
|
|
1643
|
+
stdout=subprocess.DEVNULL,
|
|
1644
|
+
stderr=subprocess.DEVNULL,
|
|
1645
|
+
)
|
|
1646
|
+
|
|
1647
|
+
# Next.js npm run dev başlat — node_modules/.bin/next'i doğrudan çağır
|
|
1648
|
+
next_bin = dashboard_kok / "node_modules" / ".bin" / "next"
|
|
1649
|
+
npm_cmd = str(next_bin) if next_bin.exists() else "npm"
|
|
1650
|
+
ui_cmd = (
|
|
1651
|
+
[npm_cmd, "dev", "--port", str(ui_port)]
|
|
1652
|
+
if next_bin.exists()
|
|
1653
|
+
else ["npm", "run", "dev", "--", "--port", str(ui_port)]
|
|
1654
|
+
)
|
|
1655
|
+
ui_proc = subprocess.Popen(
|
|
1656
|
+
ui_cmd,
|
|
1657
|
+
cwd=str(dashboard_kok),
|
|
1658
|
+
env=env,
|
|
1659
|
+
stdout=subprocess.DEVNULL,
|
|
1660
|
+
stderr=subprocess.DEVNULL,
|
|
1661
|
+
)
|
|
1662
|
+
|
|
1663
|
+
# Başlaması için bekle, sonra tarayıcıyı aç
|
|
1664
|
+
console.print("[dim]Sunucular başlatılıyor...[/dim]")
|
|
1665
|
+
time.sleep(4)
|
|
1666
|
+
|
|
1667
|
+
try:
|
|
1668
|
+
webbrowser.open(f"http://localhost:{ui_port}")
|
|
1669
|
+
console.print(f"[green]✓[/green] Tarayıcı açıldı: [link]http://localhost:{ui_port}[/link]")
|
|
1670
|
+
console.print("[dim]Ctrl+C ile durdur[/dim]\n")
|
|
1671
|
+
|
|
1672
|
+
# Her iki süreci bekle
|
|
1673
|
+
api_proc.wait()
|
|
1674
|
+
except KeyboardInterrupt:
|
|
1675
|
+
console.print("\n[yellow]Durduruluyor...[/yellow]")
|
|
1676
|
+
finally:
|
|
1677
|
+
# Her iki süreci de temizle
|
|
1678
|
+
for proc in [api_proc, ui_proc]:
|
|
1679
|
+
try:
|
|
1680
|
+
proc.terminate()
|
|
1681
|
+
proc.wait(timeout=3)
|
|
1682
|
+
except Exception:
|
|
1683
|
+
try:
|
|
1684
|
+
proc.kill()
|
|
1685
|
+
except Exception:
|
|
1686
|
+
pass
|
|
1687
|
+
console.print("[dim]CodeDNA durduruldu.[/dim]")
|
|
1688
|
+
|
|
1689
|
+
|
|
1690
|
+
# ---------------------------------------------------------------------------
|
|
1691
|
+
# codedna uninstall (bonus)
|
|
1692
|
+
# ---------------------------------------------------------------------------
|
|
1693
|
+
@app.command()
|
|
1694
|
+
def uninstall(
|
|
1695
|
+
repo: Optional[Path] = typer.Option(None, "--repo", "-r", help="Git repo dizini"),
|
|
1696
|
+
) -> None:
|
|
1697
|
+
"""CodeDNA hook'unu kaldır."""
|
|
1698
|
+
kok = repo or find_git_root()
|
|
1699
|
+
if uninstall_hook(kok):
|
|
1700
|
+
console.print("[green]✓[/green] CodeDNA kaldırıldı.")
|
|
1701
|
+
else:
|
|
1702
|
+
raise typer.Exit(1)
|
|
1703
|
+
|
|
1704
|
+
|
|
1705
|
+
# ---------------------------------------------------------------------------
|
|
1706
|
+
# codedna natureco (Pro+ özellik — NatureCo CLI entegrasyonu)
|
|
1707
|
+
# ---------------------------------------------------------------------------
|
|
1708
|
+
@app.command()
|
|
1709
|
+
def natureco(
|
|
1710
|
+
komut: Optional[str] = typer.Argument(
|
|
1711
|
+
None,
|
|
1712
|
+
help="'connect', 'sync', 'auto-scan', 'status' veya doğrudan dosya yolu",
|
|
1713
|
+
),
|
|
1714
|
+
dosya: Optional[str] = typer.Argument(
|
|
1715
|
+
None,
|
|
1716
|
+
help="Dosya yolu (sync/auto-scan için)",
|
|
1717
|
+
),
|
|
1718
|
+
) -> None:
|
|
1719
|
+
"""NatureCo CLI ile entegre ol (Pro+ plan gerekir).
|
|
1720
|
+
|
|
1721
|
+
NatureCo CLI kod ürettiğinde otomatik CodeDNA analizi yap.
|
|
1722
|
+
|
|
1723
|
+
Kullanım:
|
|
1724
|
+
codedna natureco connect # NatureCo CLI'ya bağlan
|
|
1725
|
+
codedna natureco status # Bağlantı durumu
|
|
1726
|
+
codedna natureco sync <file> # Dosyayı tarat
|
|
1727
|
+
codedna natureco auto-scan # Post-hook: commit'te otomatik scan
|
|
1728
|
+
codedna natureco disconnect # Bağlantıyı kes
|
|
1729
|
+
"""
|
|
1730
|
+
from codedna.plan import get_current_plan, Plan as PlanEnum, is_feature_available
|
|
1731
|
+
|
|
1732
|
+
# Pro+ plan gerekli
|
|
1733
|
+
if not is_feature_available("natureco_integration"):
|
|
1734
|
+
plan = get_current_plan()
|
|
1735
|
+
console.print(
|
|
1736
|
+
Panel(
|
|
1737
|
+
f"[bold yellow]🔒 NatureCo entegrasyonu Pro+ plan gerektirir.[/bold yellow]\n\n"
|
|
1738
|
+
f"Mevcut planınız: [cyan]{plan.value.upper()}[/cyan]\n\n"
|
|
1739
|
+
f"Demo için:\n"
|
|
1740
|
+
f"[dim]codedna plan demo pro[/dim]\n\n"
|
|
1741
|
+
f"Upgrade:\n"
|
|
1742
|
+
f"[dim]https://codedna.dev/pricing[/dim]",
|
|
1743
|
+
title="[bold cyan]🧬 CodeDNA — NatureCo Integration[/bold cyan]",
|
|
1744
|
+
border_style="yellow",
|
|
1745
|
+
padding=(1, 2),
|
|
1746
|
+
)
|
|
1747
|
+
)
|
|
1748
|
+
raise typer.Exit(1)
|
|
1749
|
+
|
|
1750
|
+
if not komut or komut == "status":
|
|
1751
|
+
return _natureco_status()
|
|
1752
|
+
elif komut == "connect":
|
|
1753
|
+
return _natureco_connect()
|
|
1754
|
+
elif komut == "disconnect":
|
|
1755
|
+
return _natureco_disconnect()
|
|
1756
|
+
elif komut == "sync":
|
|
1757
|
+
return _natureco_sync(dosya)
|
|
1758
|
+
elif komut == "auto-scan":
|
|
1759
|
+
return _natureco_auto_scan()
|
|
1760
|
+
else:
|
|
1761
|
+
console.print(f"[bold red]Bilinmeyen komut:[/bold red] {komut!r}")
|
|
1762
|
+
console.print("Geçerli: connect, status, sync, auto-scan, disconnect")
|
|
1763
|
+
raise typer.Exit(1)
|
|
1764
|
+
|
|
1765
|
+
|
|
1766
|
+
def _natureco_config_path() -> Path:
|
|
1767
|
+
"""NatureCo entegrasyon ayar dosyası yolu."""
|
|
1768
|
+
return Path.home() / ".codedna" / "natureco.json"
|
|
1769
|
+
|
|
1770
|
+
|
|
1771
|
+
def _natureco_status() -> None:
|
|
1772
|
+
"""Bağlantı durumunu göster."""
|
|
1773
|
+
config_path = _natureco_config_path()
|
|
1774
|
+
if config_path.exists():
|
|
1775
|
+
import json as _json
|
|
1776
|
+
data = _json.loads(config_path.read_text())
|
|
1777
|
+
console.print(
|
|
1778
|
+
Panel(
|
|
1779
|
+
f"[bold green]✓ NatureCo CLI bağlı[/bold green]\n\n"
|
|
1780
|
+
f"[bold]Hook:[/bold] {'[green]Aktif[/green]' if data.get('auto_scan') else '[yellow]Pasif[/yellow]'}\n"
|
|
1781
|
+
f"[bold]Son sync:[/bold] {data.get('last_sync', 'Hiç')}\n"
|
|
1782
|
+
f"[bold]Toplam dosya:[/bold] {data.get('total_files', 0)}\n"
|
|
1783
|
+
f"[bold]Bağlandı:[/bold] {data.get('connected_at', 'Bilinmiyor')}",
|
|
1784
|
+
title="[bold cyan]🧬 CodeDNA ↔ NatureCo[/bold cyan]",
|
|
1785
|
+
border_style="green",
|
|
1786
|
+
padding=(1, 2),
|
|
1787
|
+
)
|
|
1788
|
+
)
|
|
1789
|
+
else:
|
|
1790
|
+
console.print(
|
|
1791
|
+
Panel(
|
|
1792
|
+
"[bold yellow]NatureCo CLI henüz bağlı değil.[/bold yellow]\n\n"
|
|
1793
|
+
"Bağlamak için:\n"
|
|
1794
|
+
"[dim]codedna natureco connect[/dim]",
|
|
1795
|
+
title="[bold cyan]🧬 CodeDNA ↔ NatureCo[/bold cyan]",
|
|
1796
|
+
border_style="yellow",
|
|
1797
|
+
padding=(1, 2),
|
|
1798
|
+
)
|
|
1799
|
+
)
|
|
1800
|
+
|
|
1801
|
+
|
|
1802
|
+
def _natureco_connect() -> None:
|
|
1803
|
+
"""NatureCo CLI ile bağlantı kur."""
|
|
1804
|
+
import json as _json
|
|
1805
|
+
import time as _time
|
|
1806
|
+
|
|
1807
|
+
config_path = _natureco_config_path()
|
|
1808
|
+
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
1809
|
+
|
|
1810
|
+
# NatureCo CLI yüklü mü kontrol et
|
|
1811
|
+
nc_path = None
|
|
1812
|
+
for p in ["/usr/local/bin/natureco", "/opt/homebrew/bin/natureco",
|
|
1813
|
+
str(Path.home() / ".npm-global/bin/natureco")]:
|
|
1814
|
+
if Path(p).exists():
|
|
1815
|
+
nc_path = p
|
|
1816
|
+
break
|
|
1817
|
+
|
|
1818
|
+
# PATH'te ara
|
|
1819
|
+
if not nc_path:
|
|
1820
|
+
import shutil
|
|
1821
|
+
nc_path = shutil.which("natureco")
|
|
1822
|
+
|
|
1823
|
+
if nc_path:
|
|
1824
|
+
console.print(f"[green]✓[/green] NatureCo CLI bulundu: [dim]{nc_path}[/dim]")
|
|
1825
|
+
else:
|
|
1826
|
+
console.print("[yellow]⚠[/yellow] NatureCo CLI PATH'te bulunamadı")
|
|
1827
|
+
console.print("[dim] Yine de entegrasyon kuruldu — NatureCo CLI kurulunca çalışacak[/dim]")
|
|
1828
|
+
|
|
1829
|
+
config = {
|
|
1830
|
+
"connected": True,
|
|
1831
|
+
"natureco_cli_path": nc_path,
|
|
1832
|
+
"auto_scan": True, # Varsayılan açık
|
|
1833
|
+
"last_sync": None,
|
|
1834
|
+
"total_files": 0,
|
|
1835
|
+
"connected_at": _time.strftime("%Y-%m-%d %H:%M:%S"),
|
|
1836
|
+
}
|
|
1837
|
+
config_path.write_text(_json.dumps(config, indent=2))
|
|
1838
|
+
|
|
1839
|
+
console.print(
|
|
1840
|
+
Panel(
|
|
1841
|
+
"[bold green]✓ NatureCo CLI bağlantısı kuruldu![/bold green]\n\n"
|
|
1842
|
+
"Artık NatureCo CLI ile üretilen her kod otomatik taranacak.\n\n"
|
|
1843
|
+
"[bold]Sonraki adımlar:[/bold]\n"
|
|
1844
|
+
"1. NatureCo CLI'da kod üretin (`natureco code`)\n"
|
|
1845
|
+
"2. CodeDNA otomatik scan yapar\n"
|
|
1846
|
+
"3. `codedna natureco status` ile durumu kontrol edin",
|
|
1847
|
+
title="[bold green]🧬 CodeDNA ↔ NatureCo Bağlandı[/bold green]",
|
|
1848
|
+
border_style="green",
|
|
1849
|
+
padding=(1, 2),
|
|
1850
|
+
)
|
|
1851
|
+
)
|
|
1852
|
+
|
|
1853
|
+
|
|
1854
|
+
def _natureco_disconnect() -> None:
|
|
1855
|
+
"""Bağlantıyı kes."""
|
|
1856
|
+
config_path = _natureco_config_path()
|
|
1857
|
+
if config_path.exists():
|
|
1858
|
+
config_path.unlink()
|
|
1859
|
+
console.print("[green]✓[/green] NatureCo bağlantısı kesildi.")
|
|
1860
|
+
else:
|
|
1861
|
+
console.print("[yellow]Zaten bağlı değil.[/yellow]")
|
|
1862
|
+
|
|
1863
|
+
|
|
1864
|
+
def _natureco_sync(dosya: Optional[str]) -> None:
|
|
1865
|
+
"""Bir dosyayı CodeDNA ile tara."""
|
|
1866
|
+
import json as _json
|
|
1867
|
+
import time as _time
|
|
1868
|
+
|
|
1869
|
+
if not dosya:
|
|
1870
|
+
console.print("[bold red]Hata:[/bold red] Dosya yolu gerekli")
|
|
1871
|
+
console.print("Kullanım: [dim]codedna natureco sync <dosya>[/dim]")
|
|
1872
|
+
raise typer.Exit(1)
|
|
1873
|
+
|
|
1874
|
+
file_path = Path(dosya)
|
|
1875
|
+
if not file_path.exists():
|
|
1876
|
+
console.print(f"[bold red]Hata:[/bold red] Dosya bulunamadı: {dosya}")
|
|
1877
|
+
raise typer.Exit(1)
|
|
1878
|
+
|
|
1879
|
+
# Repo kökünü bul
|
|
1880
|
+
kok = find_git_root() or Path.cwd()
|
|
1881
|
+
db_yolu = _get_db(kok)
|
|
1882
|
+
|
|
1883
|
+
# Scoring yap (analyzer kullanarak)
|
|
1884
|
+
from codedna.analyzer import analyze_file
|
|
1885
|
+
|
|
1886
|
+
console.print(f"[dim]Taranıyor:[/dim] {file_path.name}")
|
|
1887
|
+
result = analyze_file(file_path, single_commit_ratio=0.0)
|
|
1888
|
+
|
|
1889
|
+
if result:
|
|
1890
|
+
console.print(
|
|
1891
|
+
Panel(
|
|
1892
|
+
f"[bold green]✓ Tarama tamamlandı[/bold green]\n\n"
|
|
1893
|
+
f"[bold]Dosya:[/bold] {file_path.name}\n"
|
|
1894
|
+
f"[bold]AI olasılığı:[/bold] [cyan]%{result.ai_probability * 100:.0f}[/cyan]\n"
|
|
1895
|
+
f"[bold]Karmaşıklık:[/bold] {result.complexity_score:.1f}\n"
|
|
1896
|
+
f"[bold]Yorum oranı:[/bold] {result.comment_ratio * 100:.0f}%\n",
|
|
1897
|
+
title="[bold cyan]🧬 Tarama Sonucu[/bold cyan]",
|
|
1898
|
+
border_style="cyan",
|
|
1899
|
+
padding=(1, 2),
|
|
1900
|
+
)
|
|
1901
|
+
)
|
|
1902
|
+
|
|
1903
|
+
# Config'i güncelle
|
|
1904
|
+
config_path = _natureco_config_path()
|
|
1905
|
+
if config_path.exists():
|
|
1906
|
+
cfg = _json.loads(config_path.read_text())
|
|
1907
|
+
cfg["last_sync"] = _time.strftime("%Y-%m-%d %H:%M:%S")
|
|
1908
|
+
cfg["total_files"] = cfg.get("total_files", 0) + 1
|
|
1909
|
+
config_path.write_text(_json.dumps(cfg, indent=2))
|
|
1910
|
+
else:
|
|
1911
|
+
console.print("[yellow]Dosya analiz edilemedi.[/yellow]")
|
|
1912
|
+
|
|
1913
|
+
|
|
1914
|
+
def _natureco_auto_scan() -> None:
|
|
1915
|
+
"""Auto-scan hook'unu aktif/pasif yap."""
|
|
1916
|
+
import json as _json
|
|
1917
|
+
|
|
1918
|
+
config_path = _natureco_config_path()
|
|
1919
|
+
if not config_path.exists():
|
|
1920
|
+
console.print("[bold red]Hata:[/bold red] Önce connect gerekli")
|
|
1921
|
+
console.print("[dim]codedna natureco connect[/dim]")
|
|
1922
|
+
raise typer.Exit(1)
|
|
1923
|
+
|
|
1924
|
+
cfg = _json.loads(config_path.read_text())
|
|
1925
|
+
cfg["auto_scan"] = not cfg.get("auto_scan", False)
|
|
1926
|
+
config_path.write_text(_json.dumps(cfg, indent=2))
|
|
1927
|
+
|
|
1928
|
+
state = "[green]aktif[/green]" if cfg["auto_scan"] else "[yellow]pasif[/yellow]"
|
|
1929
|
+
console.print(f"[green]✓[/green] Auto-scan {state}")
|
|
1930
|
+
|
|
1931
|
+
|
|
1932
|
+
# ---------------------------------------------------------------------------
|
|
1933
|
+
# Yardımcı fonksiyonlar
|
|
1934
|
+
# ---------------------------------------------------------------------------
|
|
1935
|
+
def _kisalt_yol(tam_yol: str, kok: str) -> str:
|
|
1936
|
+
"""Uzun yolları repo köküne göre kısalt."""
|
|
1937
|
+
try:
|
|
1938
|
+
goreceli = Path(tam_yol).relative_to(Path(kok))
|
|
1939
|
+
yol_str = str(goreceli)
|
|
1940
|
+
if len(yol_str) > 40:
|
|
1941
|
+
parcalar = Path(yol_str).parts
|
|
1942
|
+
if len(parcalar) > 3:
|
|
1943
|
+
return f".../{'/'.join(parcalar[-2:])}"
|
|
1944
|
+
return yol_str
|
|
1945
|
+
except ValueError:
|
|
1946
|
+
return tam_yol[-40:] if len(tam_yol) > 40 else tam_yol
|
|
1947
|
+
|
|
1948
|
+
|
|
1949
|
+
def _risk_etiketi(yuzde: float) -> tuple[str, str]:
|
|
1950
|
+
"""AI yüzdesine göre risk etiketi ve renk döndür."""
|
|
1951
|
+
if yuzde >= 70:
|
|
1952
|
+
return "YÜKSEK", "red"
|
|
1953
|
+
elif yuzde >= 40:
|
|
1954
|
+
return "ORTA", "yellow"
|
|
1955
|
+
else:
|
|
1956
|
+
return "DÜŞÜK", "green"
|
|
1957
|
+
|
|
1958
|
+
|
|
1959
|
+
def main() -> None:
|
|
1960
|
+
"""CLI giriş noktası."""
|
|
1961
|
+
app()
|
|
1962
|
+
|
|
1963
|
+
|
|
1964
|
+
if __name__ == "__main__":
|
|
1965
|
+
main()
|