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/db.py ADDED
@@ -0,0 +1,336 @@
1
+ """SQLite veritabanı CRUD işlemleri."""
2
+
3
+ import sqlite3
4
+ from contextlib import contextmanager
5
+ from datetime import datetime
6
+ from pathlib import Path
7
+ from typing import Generator, Optional
8
+
9
+ # Varsayılan veritabanı yolu
10
+ DB_PATH = Path.home() / ".codedna" / "codedna.db"
11
+
12
+
13
+ def get_db_path(repo_path: Optional[Path] = None) -> Path:
14
+ """Repo'ya özgü veritabanı yolunu döndür."""
15
+ if repo_path:
16
+ return repo_path / ".codedna.db"
17
+ return DB_PATH
18
+
19
+
20
+ @contextmanager
21
+ def get_connection(db_path: Optional[Path] = None) -> Generator[sqlite3.Connection, None, None]:
22
+ """SQLite bağlantısını context manager ile yönet."""
23
+ path = db_path or DB_PATH
24
+ path.parent.mkdir(parents=True, exist_ok=True)
25
+ conn = sqlite3.connect(str(path))
26
+ conn.row_factory = sqlite3.Row
27
+ try:
28
+ yield conn
29
+ conn.commit()
30
+ except Exception:
31
+ conn.rollback()
32
+ raise
33
+ finally:
34
+ conn.close()
35
+
36
+
37
+ def init_db(db_path: Optional[Path] = None) -> None:
38
+ """Veritabanı şemasını oluştur (yoksa)."""
39
+ with get_connection(db_path) as conn:
40
+ conn.executescript("""
41
+ CREATE TABLE IF NOT EXISTS commits (
42
+ id INTEGER PRIMARY KEY,
43
+ commit_hash TEXT UNIQUE,
44
+ author TEXT,
45
+ timestamp INTEGER,
46
+ files_changed INTEGER,
47
+ understanding_score REAL,
48
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
49
+ );
50
+
51
+ CREATE TABLE IF NOT EXISTS file_scores (
52
+ id INTEGER PRIMARY KEY,
53
+ commit_hash TEXT,
54
+ file_path TEXT,
55
+ ai_probability REAL,
56
+ complexity_score REAL,
57
+ comment_ratio REAL,
58
+ understanding_score REAL,
59
+ ai_tool_guess TEXT,
60
+ FOREIGN KEY (commit_hash) REFERENCES commits(commit_hash)
61
+ );
62
+
63
+ CREATE TABLE IF NOT EXISTS file_ownership (
64
+ id INTEGER PRIMARY KEY,
65
+ file_path TEXT,
66
+ author TEXT,
67
+ lines_owned INTEGER,
68
+ last_touched INTEGER,
69
+ avg_understanding REAL,
70
+ UNIQUE(file_path, author)
71
+ );
72
+
73
+ CREATE TABLE IF NOT EXISTS sprints (
74
+ id INTEGER PRIMARY KEY,
75
+ sprint_name TEXT,
76
+ start_date INTEGER,
77
+ end_date INTEGER,
78
+ total_lines_ai INTEGER,
79
+ total_lines_human INTEGER,
80
+ avg_understanding REAL,
81
+ debt_delta_hours REAL,
82
+ health_score REAL,
83
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
84
+ );
85
+
86
+ CREATE TABLE IF NOT EXISTS protected_modules (
87
+ id INTEGER PRIMARY KEY,
88
+ file_path TEXT UNIQUE,
89
+ min_understanding_threshold REAL DEFAULT 3.5,
90
+ label TEXT,
91
+ added_by TEXT,
92
+ added_at INTEGER,
93
+ last_alert_at INTEGER,
94
+ is_active INTEGER DEFAULT 1
95
+ );
96
+
97
+ CREATE TABLE IF NOT EXISTS interview_sessions (
98
+ id INTEGER PRIMARY KEY,
99
+ candidate_name TEXT,
100
+ file_path TEXT,
101
+ started_at INTEGER,
102
+ completed_at INTEGER,
103
+ questions_json TEXT,
104
+ comprehension_score REAL,
105
+ time_taken_seconds INTEGER,
106
+ evaluator_notes TEXT,
107
+ created_by TEXT
108
+ );
109
+ """)
110
+
111
+
112
+ def save_commit(
113
+ commit_hash: str,
114
+ author: str,
115
+ timestamp: int,
116
+ files_changed: int,
117
+ understanding_score: Optional[float],
118
+ db_path: Optional[Path] = None,
119
+ ) -> None:
120
+ """Commit bilgisini kaydet veya güncelle."""
121
+ with get_connection(db_path) as conn:
122
+ conn.execute(
123
+ """
124
+ INSERT INTO commits (commit_hash, author, timestamp, files_changed, understanding_score)
125
+ VALUES (?, ?, ?, ?, ?)
126
+ ON CONFLICT(commit_hash) DO UPDATE SET
127
+ understanding_score = excluded.understanding_score
128
+ """,
129
+ (commit_hash, author, timestamp, files_changed, understanding_score),
130
+ )
131
+
132
+
133
+ def save_file_score(
134
+ commit_hash: str,
135
+ file_path: str,
136
+ ai_probability: float,
137
+ complexity_score: float,
138
+ comment_ratio: float,
139
+ understanding_score: Optional[float] = None,
140
+ db_path: Optional[Path] = None,
141
+ ) -> None:
142
+ """Dosya analiz skorunu kaydet."""
143
+ with get_connection(db_path) as conn:
144
+ conn.execute(
145
+ """
146
+ INSERT INTO file_scores
147
+ (commit_hash, file_path, ai_probability, complexity_score, comment_ratio, understanding_score)
148
+ VALUES (?, ?, ?, ?, ?, ?)
149
+ """,
150
+ (commit_hash, file_path, ai_probability, complexity_score, comment_ratio, understanding_score),
151
+ )
152
+
153
+
154
+ def get_commit_history(limit: int = 20, db_path: Optional[Path] = None) -> list[sqlite3.Row]:
155
+ """Son N commit'i tarihe göre sıralı getir."""
156
+ with get_connection(db_path) as conn:
157
+ rows = conn.execute(
158
+ """
159
+ SELECT * FROM commits
160
+ ORDER BY timestamp DESC
161
+ LIMIT ?
162
+ """,
163
+ (limit,),
164
+ ).fetchall()
165
+ return rows
166
+
167
+
168
+ def get_file_scores_for_commit(commit_hash: str, db_path: Optional[Path] = None) -> list[sqlite3.Row]:
169
+ """Belirli bir commit'e ait dosya skorlarını getir."""
170
+ with get_connection(db_path) as conn:
171
+ rows = conn.execute(
172
+ "SELECT * FROM file_scores WHERE commit_hash = ?",
173
+ (commit_hash,),
174
+ ).fetchall()
175
+ return rows
176
+
177
+
178
+ def get_latest_commit(db_path: Optional[Path] = None) -> Optional[sqlite3.Row]:
179
+ """En son kaydedilen commit'i getir."""
180
+ with get_connection(db_path) as conn:
181
+ row = conn.execute(
182
+ "SELECT * FROM commits ORDER BY timestamp DESC LIMIT 1"
183
+ ).fetchone()
184
+ return row
185
+
186
+
187
+ def get_latest_understanding_for_file(
188
+ file_path: str, db_path: Optional[Path] = None
189
+ ) -> Optional[float]:
190
+ """Belirli bir dosyanın en son anlama skorunu getir."""
191
+ with get_connection(db_path) as conn:
192
+ row = conn.execute(
193
+ """
194
+ SELECT fs.understanding_score
195
+ FROM file_scores fs
196
+ JOIN commits c ON fs.commit_hash = c.commit_hash
197
+ WHERE fs.file_path = ? AND fs.understanding_score IS NOT NULL
198
+ ORDER BY c.timestamp DESC
199
+ LIMIT 1
200
+ """,
201
+ (file_path,),
202
+ ).fetchone()
203
+ return float(row["understanding_score"]) if row else None
204
+
205
+
206
+ def get_all_file_understanding_scores(
207
+ db_path: Optional[Path] = None,
208
+ ) -> dict[str, float]:
209
+ """
210
+ Tüm dosyaların en son anlama skorlarını dict olarak getir.
211
+ {file_path: understanding_score} formatında döner.
212
+ """
213
+ with get_connection(db_path) as conn:
214
+ rows = conn.execute(
215
+ """
216
+ SELECT fs.file_path, fs.understanding_score
217
+ FROM file_scores fs
218
+ JOIN commits c ON fs.commit_hash = c.commit_hash
219
+ WHERE fs.understanding_score IS NOT NULL
220
+ GROUP BY fs.file_path
221
+ HAVING c.timestamp = MAX(c.timestamp)
222
+ """,
223
+ ).fetchall()
224
+ return {r["file_path"]: float(r["understanding_score"]) for r in rows}
225
+
226
+
227
+ def update_understanding_score(
228
+ commit_hash: str,
229
+ understanding_score: float,
230
+ db_path: Optional[Path] = None,
231
+ ) -> None:
232
+ """Commit'in ve ilgili dosyaların anlama skorunu güncelle."""
233
+ with get_connection(db_path) as conn:
234
+ conn.execute(
235
+ "UPDATE commits SET understanding_score = ? WHERE commit_hash = ?",
236
+ (understanding_score, commit_hash),
237
+ )
238
+ # Aynı commit'e ait tüm dosya skorlarını da güncelle
239
+ conn.execute(
240
+ "UPDATE file_scores SET understanding_score = ? WHERE commit_hash = ?",
241
+ (understanding_score, commit_hash),
242
+ )
243
+
244
+
245
+ def upsert_file_ownership(
246
+ file_path: str,
247
+ author: str,
248
+ lines_owned: int,
249
+ last_touched: int,
250
+ avg_understanding: Optional[float] = None,
251
+ db_path: Optional[Path] = None,
252
+ ) -> None:
253
+ """Dosya sahiplik kaydını ekle veya güncelle."""
254
+ with get_connection(db_path) as conn:
255
+ conn.execute(
256
+ """
257
+ INSERT INTO file_ownership
258
+ (file_path, author, lines_owned, last_touched, avg_understanding)
259
+ VALUES (?, ?, ?, ?, ?)
260
+ ON CONFLICT(file_path, author) DO UPDATE SET
261
+ lines_owned = excluded.lines_owned,
262
+ last_touched = excluded.last_touched,
263
+ avg_understanding = COALESCE(excluded.avg_understanding, avg_understanding)
264
+ """,
265
+ (file_path, author, lines_owned, last_touched, avg_understanding),
266
+ )
267
+
268
+
269
+ def get_file_ownership(
270
+ file_path: Optional[str] = None,
271
+ db_path: Optional[Path] = None,
272
+ ) -> list[sqlite3.Row]:
273
+ """
274
+ Dosya sahiplik kayıtlarını getir.
275
+
276
+ Args:
277
+ file_path: Belirli bir dosya filtrele (None ise tüm dosyalar)
278
+ """
279
+ with get_connection(db_path) as conn:
280
+ if file_path:
281
+ rows = conn.execute(
282
+ "SELECT * FROM file_ownership WHERE file_path = ? ORDER BY lines_owned DESC",
283
+ (file_path,),
284
+ ).fetchall()
285
+ else:
286
+ rows = conn.execute(
287
+ "SELECT * FROM file_ownership ORDER BY file_path, lines_owned DESC"
288
+ ).fetchall()
289
+ return rows
290
+
291
+
292
+ def save_sprint(
293
+ sprint_name: str,
294
+ start_date: int,
295
+ end_date: int,
296
+ total_lines_ai: int,
297
+ total_lines_human: int,
298
+ avg_understanding: Optional[float],
299
+ debt_delta_hours: Optional[float],
300
+ health_score: Optional[float],
301
+ db_path: Optional[Path] = None,
302
+ ) -> int:
303
+ """Sprint kaydı oluştur, yeni kaydın id'sini döndür."""
304
+ with get_connection(db_path) as conn:
305
+ cur = conn.execute(
306
+ """
307
+ INSERT INTO sprints
308
+ (sprint_name, start_date, end_date, total_lines_ai, total_lines_human,
309
+ avg_understanding, debt_delta_hours, health_score)
310
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
311
+ """,
312
+ (sprint_name, start_date, end_date, total_lines_ai, total_lines_human,
313
+ avg_understanding, debt_delta_hours, health_score),
314
+ )
315
+ return cur.lastrowid or 0
316
+
317
+
318
+ def get_sprint_history(
319
+ limit: int = 10,
320
+ db_path: Optional[Path] = None,
321
+ ) -> list[sqlite3.Row]:
322
+ """Geçmiş sprint'leri tarihe göre sıralı getir."""
323
+ with get_connection(db_path) as conn:
324
+ rows = conn.execute(
325
+ "SELECT * FROM sprints ORDER BY start_date DESC LIMIT ?",
326
+ (limit,),
327
+ ).fetchall()
328
+ return rows
329
+
330
+
331
+ def get_latest_sprint(db_path: Optional[Path] = None) -> Optional[sqlite3.Row]:
332
+ """En son sprint kaydını getir."""
333
+ with get_connection(db_path) as conn:
334
+ return conn.execute(
335
+ "SELECT * FROM sprints ORDER BY start_date DESC LIMIT 1"
336
+ ).fetchone()
codedna/git_hook.py ADDED
@@ -0,0 +1,212 @@
1
+ """Git hook kurulum ve yönetim modülü."""
2
+
3
+ import os
4
+ import stat
5
+ from pathlib import Path
6
+ from typing import Optional
7
+
8
+ from rich.console import Console
9
+
10
+ console = Console()
11
+
12
+ # Post-commit hook içeriği
13
+ HOOK_TEMPLATE = """#!/bin/bash
14
+ # CodeDNA post-commit hook
15
+ # Otomatik olarak codedna tarafından oluşturuldu.
16
+ #
17
+ # Anketin çalışması için stdin'i doğrudan terminale (/dev/tty) bağla.
18
+ # Git hook'ları bazı durumlarda stdin'i kapalı/boş bir akışa yönlendirir;
19
+ # bu satır olmadan IntPrompt anında EOFError alıp anketi sessizce atlıyor.
20
+ #
21
+ # Koşullar:
22
+ # [ -t 1 ] → çıkış bir terminale bağlı (CI/pipe'da false → anket sorulmaz, bu doğru)
23
+ # [ -r /dev/tty ] → /dev/tty gerçekten okunabilir
24
+ if [ -t 1 ] && [ -r /dev/tty ]; then
25
+ exec < /dev/tty
26
+ fi
27
+
28
+ # codedna'nın PATH'te olup olmadığını kontrol et
29
+ if command -v codedna &> /dev/null; then
30
+ codedna status --hook
31
+ else
32
+ # uv ile çalıştırmayı dene (geliştirme ortamı için)
33
+ REPO_ROOT="$(git rev-parse --show-toplevel)"
34
+ if [ -f "$REPO_ROOT/.venv/bin/codedna" ]; then
35
+ "$REPO_ROOT/.venv/bin/codedna" status --hook
36
+ elif [ -f "$HOME/.local/bin/codedna" ]; then
37
+ "$HOME/.local/bin/codedna" status --hook
38
+ else
39
+ echo "[CodeDNA] 'codedna' komutu bulunamadı. 'pip install codedna' deneyin."
40
+ fi
41
+ fi
42
+ """
43
+
44
+
45
+ def find_git_root(start_path: Optional[Path] = None) -> Optional[Path]:
46
+ """
47
+ Verilen yoldan yukarı doğru .git dizinini ara.
48
+
49
+ Args:
50
+ start_path: Arama başlangıç noktası (None ise mevcut dizin)
51
+
52
+ Returns:
53
+ .git dizinini içeren repo kökü veya None
54
+ """
55
+ yol = Path(start_path or Path.cwd()).resolve()
56
+ for ebeveyn in [yol, *yol.parents]:
57
+ if (ebeveyn / ".git").exists():
58
+ return ebeveyn
59
+ return None
60
+
61
+
62
+ def install_hook(repo_path: Optional[Path] = None) -> bool:
63
+ """
64
+ Post-commit hook'u kur.
65
+
66
+ Args:
67
+ repo_path: Git repo kök dizini (None ise otomatik bul)
68
+
69
+ Returns:
70
+ Başarıyla kurulduysa True
71
+ """
72
+ kok = repo_path or find_git_root()
73
+ if not kok:
74
+ console.print("[bold red]Hata:[/bold red] Git repo bulunamadı. 'git init' ile başlatın.")
75
+ return False
76
+
77
+ hooks_dir = kok / ".git" / "hooks"
78
+ hooks_dir.mkdir(parents=True, exist_ok=True)
79
+ hook_dosyasi = hooks_dir / "post-commit"
80
+
81
+ # Mevcut hook varsa yedekle
82
+ if hook_dosyasi.exists():
83
+ yedek = hooks_dir / "post-commit.backup"
84
+ hook_dosyasi.rename(yedek)
85
+ console.print(f"[yellow]Mevcut hook yedeklendi:[/yellow] {yedek}")
86
+
87
+ # Yeni hook'u yaz
88
+ hook_dosyasi.write_text(HOOK_TEMPLATE, encoding="utf-8")
89
+
90
+ # Çalıştırılabilir yap (chmod +x)
91
+ mevcut_mod = hook_dosyasi.stat().st_mode
92
+ hook_dosyasi.chmod(mevcut_mod | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
93
+
94
+ console.print(f"[green]✓[/green] Hook kuruldu: {hook_dosyasi}")
95
+ return True
96
+
97
+
98
+ def uninstall_hook(repo_path: Optional[Path] = None) -> bool:
99
+ """
100
+ Post-commit hook'u kaldır.
101
+
102
+ Args:
103
+ repo_path: Git repo kök dizini (None ise otomatik bul)
104
+
105
+ Returns:
106
+ Başarıyla kaldırıldıysa True
107
+ """
108
+ kok = repo_path or find_git_root()
109
+ if not kok:
110
+ console.print("[bold red]Hata:[/bold red] Git repo bulunamadı.")
111
+ return False
112
+
113
+ hook_dosyasi = kok / ".git" / "hooks" / "post-commit"
114
+
115
+ if not hook_dosyasi.exists():
116
+ console.print("[yellow]Post-commit hook bulunamadı, zaten kaldırılmış.[/yellow]")
117
+ return True
118
+
119
+ # Yedekten geri yükle
120
+ yedek = kok / ".git" / "hooks" / "post-commit.backup"
121
+ if yedek.exists():
122
+ hook_dosyasi.unlink()
123
+ yedek.rename(hook_dosyasi)
124
+ console.print("[green]✓[/green] Önceki hook geri yüklendi.")
125
+ else:
126
+ hook_dosyasi.unlink()
127
+ console.print("[green]✓[/green] CodeDNA hook kaldırıldı.")
128
+
129
+ return True
130
+
131
+
132
+ def install_ci_workflow(repo_path: Optional[Path] = None) -> bool:
133
+ """
134
+ GitHub Actions CI şablonunu repo'ya yaz.
135
+
136
+ Args:
137
+ repo_path: Git repo kök dizini (None ise otomatik bul)
138
+
139
+ Returns:
140
+ Başarıyla yazıldıysa True
141
+ """
142
+ import importlib.resources
143
+
144
+ kok = repo_path or find_git_root()
145
+ if not kok:
146
+ console.print("[bold red]Hata:[/bold red] Git repo bulunamadı.")
147
+ return False
148
+
149
+ workflows_dir = kok / ".github" / "workflows"
150
+ workflows_dir.mkdir(parents=True, exist_ok=True)
151
+ hedef = workflows_dir / "codedna.yml"
152
+
153
+ if hedef.exists():
154
+ console.print("[yellow]⚠[/yellow] .github/workflows/codedna.yml zaten mevcut, üzerine yazılmıyor.")
155
+ return True
156
+
157
+ # Paketle birlikte gelen şablonu kopyala
158
+ try:
159
+ sablon_yolu = Path(__file__).parent.parent / ".github" / "workflows" / "codedna.yml"
160
+ if sablon_yolu.exists():
161
+ hedef.write_text(sablon_yolu.read_text(encoding="utf-8"), encoding="utf-8")
162
+ else:
163
+ # Fallback: inline şablon
164
+ hedef.write_text(_CI_SABLON, encoding="utf-8")
165
+ console.print(f"[green]✓[/green] CI şablonu oluşturuldu: [dim]{hedef}[/dim]")
166
+ return True
167
+ except Exception as e:
168
+ console.print(f"[red]✗[/red] CI şablonu yazılamadı: {e}")
169
+ return False
170
+
171
+
172
+ # GitHub Actions şablon içeriği (fallback)
173
+ _CI_SABLON = """\
174
+ name: CodeDNA Analysis
175
+
176
+ on:
177
+ push:
178
+ branches: ["**"]
179
+ pull_request:
180
+ branches: ["**"]
181
+
182
+ jobs:
183
+ analyze:
184
+ name: AI Kod Analizi
185
+ runs-on: ubuntu-latest
186
+ steps:
187
+ - uses: actions/checkout@v4
188
+ with:
189
+ fetch-depth: 0
190
+ - uses: actions/setup-python@v5
191
+ with:
192
+ python-version: "3.10"
193
+ - run: pip install codedna
194
+ - run: codedna scan
195
+ continue-on-error: true
196
+ - name: Yüksek risk kontrolü
197
+ run: |
198
+ codedna scan --min-risk 0.7 && echo "✅ Yüksek riskli dosya yok." || echo "⚠️ Yüksek riskli dosyalar tespit edildi."
199
+ continue-on-error: true
200
+ """
201
+
202
+
203
+ def is_hook_installed(repo_path: Optional[Path] = None) -> bool:
204
+ """CodeDNA hook'unun kurulu olup olmadığını kontrol et."""
205
+ kok = repo_path or find_git_root()
206
+ if not kok:
207
+ return False
208
+ hook_dosyasi = kok / ".git" / "hooks" / "post-commit"
209
+ if not hook_dosyasi.exists():
210
+ return False
211
+ icerik = hook_dosyasi.read_text(encoding="utf-8", errors="replace")
212
+ return "CodeDNA" in icerik
@@ -0,0 +1 @@
1
+ """CodeDNA dış servis entegrasyonları."""