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 ADDED
@@ -0,0 +1,4 @@
1
+ """CodeDNA — AI Kod Şeffaflık Aracı."""
2
+
3
+ __version__ = "0.1.0"
4
+ __app_name__ = "codedna"
@@ -0,0 +1,223 @@
1
+ """
2
+ Farklı AI kod asistanlarının bıraktığı örüntüleri ayırt eder.
3
+
4
+ ÖNEMLİ UYARI:
5
+ Bu kesin bir tespit DEĞİL — örüntü tabanlı sezgisel bir TAHMİN modelidir.
6
+ Sonuçlar yanlış pozitif/negatif içerebilir. Kesinlik iddia edilmez.
7
+ Kullanıcıya bu bağlamda sunulmalıdır.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import re
13
+ from dataclasses import dataclass
14
+ from pathlib import Path
15
+ from typing import Optional
16
+
17
+ from codedna.db import get_connection
18
+
19
+
20
+ # ---------------------------------------------------------------------------
21
+ # Araç örüntü tanımları — sezgisel, savunulabilir ama kesin değil
22
+ # ---------------------------------------------------------------------------
23
+
24
+ # Her araç için ağırlıklı örüntü listesi: (regex_pattern, ağırlık)
25
+ _ARAC_ORNUNTULERI: dict[str, list[tuple[str, float]]] = {
26
+ "copilot": [
27
+ # GitHub Copilot: kısa, özlü satır içi yorumlar, tip bildirimleri yok
28
+ (r"#\s+[A-Z][a-z].{5,40}$", 0.15), # tek satır başlık yorum
29
+ (r"def \w+\([^)]{0,30}\):\s*$", 0.10), # parametresiz/minimal fonksiyon
30
+ (r"#\s+TODO:", 0.10), # TODO yorumları
31
+ (r"^\s{4}pass\s*$", 0.08), # pass ile biten fonksiyonlar
32
+ (r"return \w+\.get\(", 0.07), # .get() pattern
33
+ ],
34
+ "cursor": [
35
+ # Cursor: detaylı docstring, tip ipucu zenginliği
36
+ (r'"""[\s\S]{20,200}"""', 0.20), # uzun docstring
37
+ (r":\s*(str|int|float|bool|list|dict|Optional)", 0.15), # tip ipuçları
38
+ (r"->.*:\s*$", 0.12), # dönüş tipi bildirimi
39
+ (r"from typing import", 0.10), # typing modülü
40
+ (r"@dataclass", 0.10), # dataclass kullanımı
41
+ ],
42
+ "claude": [
43
+ # Claude: yapılandırılmış çok satırlı açıklamalar, Türkçe/çok dilli yorum
44
+ (r"#\s+\d+\.\s+\w", 0.18), # numaralı adım yorumları
45
+ (r"\"\"\"[\s\S]*Args:[\s\S]*Returns:", 0.20), # Args/Returns docstring
46
+ (r"#\s+─{3,}", 0.15), # ayırıcı çizgi yorumlar
47
+ (r"raise \w+Error\(f[\"']", 0.10), # f-string hata mesajları
48
+ (r"from __future__ import annotations", 0.12), # modern annotation
49
+ ],
50
+ }
51
+
52
+ # Minimum güven eşiği — altındaysa "unknown" döndür
53
+ _MIN_GUVEN = 0.15
54
+
55
+
56
+ @dataclass
57
+ class AIAracTahmini:
58
+ """Tek dosya için AI araç tahmini."""
59
+
60
+ arac: str # "copilot" | "cursor" | "claude" | "unknown"
61
+ guven: float # 0.0–1.0
62
+ puan_detayi: dict[str, float] # araç → ham puan
63
+ uyari: str = (
64
+ "Bu tespit örüntü tabanlı bir tahmindir — kesin değildir."
65
+ )
66
+
67
+
68
+ def guess_ai_tool(file_path: str, code: str) -> AIAracTahmini:
69
+ """
70
+ Dosya için olası AI aracı tahmini ve güven skoru döndür.
71
+
72
+ Strateji:
73
+ Her araç için tanımlı regex örüntüleri koda uygulanır, ağırlıklı
74
+ eşleşme sayısına göre toplam puan hesaplanır. En yüksek puanlı
75
+ araç, minimum güven eşiğini geçiyorsa seçilir.
76
+
77
+ Args:
78
+ file_path: Dosya yolu (uzantı filtresi için kullanılır)
79
+ code: Dosyanın kaynak kodu
80
+
81
+ Returns:
82
+ AIAracTahmini nesnesi
83
+ """
84
+ satirlar = code.splitlines()
85
+ puan: dict[str, float] = {arac: 0.0 for arac in _ARAC_ORNUNTULERI}
86
+
87
+ for arac, ornuntular in _ARAC_ORNUNTULERI.items():
88
+ for desen, agirlik in ornuntular:
89
+ eslesme_sayisi = sum(
90
+ 1 for satir in satirlar if re.search(desen, satir)
91
+ )
92
+ # Satır sayısına normalize et (büyük dosyalarda haksız avantajı engelle)
93
+ norm = eslesme_sayisi / max(len(satirlar), 1)
94
+ puan[arac] += norm * agirlik * 10 # 0-10 arası ölçek
95
+
96
+ # Normalize et — toplam puana göre güven hesapla
97
+ toplam = sum(puan.values())
98
+ if toplam < 0.01:
99
+ return AIAracTahmini(
100
+ arac="unknown",
101
+ guven=0.0,
102
+ puan_detayi={k: round(v, 3) for k, v in puan.items()},
103
+ )
104
+
105
+ en_iyi_arac = max(puan, key=lambda k: puan[k])
106
+ guven = puan[en_iyi_arac] / toplam
107
+
108
+ # Minimum eşiği geçemiyen → unknown
109
+ if guven < _MIN_GUVEN:
110
+ en_iyi_arac = "unknown"
111
+
112
+ return AIAracTahmini(
113
+ arac=en_iyi_arac,
114
+ guven=round(guven, 3),
115
+ puan_detayi={k: round(v, 3) for k, v in puan.items()},
116
+ )
117
+
118
+
119
+ def analyze_repo_tools(
120
+ repo_path: Path,
121
+ db_path: Path,
122
+ ) -> dict[str, dict[str, float]]:
123
+ """
124
+ Repo genelinde araç bazlı dosya analizi yap ve sonuçları DB'ye kaydet.
125
+
126
+ Returns:
127
+ {arac: {"dosya_sayisi": N, "avg_ai_probability": X}} sözlüğü
128
+ """
129
+ from codedna.scorer import scan_repository
130
+ from codedna.db import get_connection
131
+
132
+ desteklenen = {".py", ".js", ".jsx", ".ts", ".tsx"}
133
+ sonuclar = scan_repository(repo_path, max_files=200)
134
+
135
+ # Araç sayaçları
136
+ arac_istatistik: dict[str, dict[str, list]] = {
137
+ "copilot": {"ai_prob": [], "understanding": []},
138
+ "cursor": {"ai_prob": [], "understanding": []},
139
+ "claude": {"ai_prob": [], "understanding": []},
140
+ "unknown": {"ai_prob": [], "understanding": []},
141
+ }
142
+
143
+ for sonuc in sonuclar:
144
+ if Path(sonuc.file_path).suffix.lower() not in desteklenen:
145
+ continue
146
+ try:
147
+ kod = Path(sonuc.file_path).read_text(encoding="utf-8", errors="replace")
148
+ except Exception:
149
+ continue
150
+
151
+ tahmin = guess_ai_tool(sonuc.file_path, kod)
152
+
153
+ # DB'ye kaydet — en son file_score kaydını güncelle
154
+ try:
155
+ with get_connection(db_path) as conn:
156
+ conn.execute(
157
+ """
158
+ UPDATE file_scores
159
+ SET ai_tool_guess = ?
160
+ WHERE file_path = ?
161
+ AND id = (
162
+ SELECT id FROM file_scores
163
+ WHERE file_path = ?
164
+ ORDER BY id DESC LIMIT 1
165
+ )
166
+ """,
167
+ (tahmin.arac, sonuc.file_path, sonuc.file_path),
168
+ )
169
+ except Exception:
170
+ pass
171
+
172
+ if tahmin.arac in arac_istatistik:
173
+ arac_istatistik[tahmin.arac]["ai_prob"].append(sonuc.ai_probability)
174
+ else:
175
+ arac_istatistik["unknown"]["ai_prob"].append(sonuc.ai_probability)
176
+
177
+ # DB'den anlama skorlarını araç bazlı topla
178
+ try:
179
+ with get_connection(db_path) as conn:
180
+ rows = conn.execute(
181
+ """
182
+ SELECT fs.ai_tool_guess, fs.understanding_score
183
+ FROM file_scores fs
184
+ WHERE fs.ai_tool_guess IS NOT NULL
185
+ AND fs.understanding_score IS NOT NULL
186
+ """
187
+ ).fetchall()
188
+ for r in rows:
189
+ arac = r["ai_tool_guess"] or "unknown"
190
+ if arac in arac_istatistik:
191
+ arac_istatistik[arac]["understanding"].append(
192
+ float(r["understanding_score"])
193
+ )
194
+ except Exception:
195
+ pass
196
+
197
+ # Sonuçları hesapla
198
+ cikti: dict[str, dict[str, float]] = {}
199
+ for arac, veri in arac_istatistik.items():
200
+ if not veri["ai_prob"]:
201
+ continue
202
+ avg_ai = sum(veri["ai_prob"]) / len(veri["ai_prob"])
203
+ avg_und = (
204
+ sum(veri["understanding"]) / len(veri["understanding"])
205
+ if veri["understanding"] else None
206
+ )
207
+ cikti[arac] = {
208
+ "dosya_sayisi": len(veri["ai_prob"]),
209
+ "avg_ai_probability": round(avg_ai, 3),
210
+ "avg_understanding": round(avg_und, 2) if avg_und is not None else None,
211
+ }
212
+
213
+ return cikti
214
+
215
+
216
+ def compare_tools_in_repo(repo_path: Path, db_path: Path) -> dict:
217
+ """
218
+ Repo genelinde araç bazlı ortalama anlama skoru ve AI olasılığı karşılaştırması.
219
+
220
+ Returns:
221
+ {"copilot": {...}, "cursor": {...}, ...} sözlüğü
222
+ """
223
+ return analyze_repo_tools(repo_path, db_path)
codedna/analyzer.py ADDED
@@ -0,0 +1,245 @@
1
+ """AST analizi ve AI imza tespiti modülü."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from dataclasses import dataclass, field
7
+ from pathlib import Path
8
+ from typing import Optional
9
+
10
+ import tree_sitter_python as tspython
11
+ import tree_sitter_javascript as tsjavascript
12
+ from tree_sitter import Language, Parser, Node
13
+
14
+ # TypeScript parser — kurulu değilse JS parser'a geri dön
15
+ try:
16
+ import tree_sitter_typescript as tstypescript
17
+ _TS_LANG = tstypescript.language_typescript()
18
+ _TSX_LANG = tstypescript.language_tsx()
19
+ _TS_AVAILABLE = True
20
+ except Exception:
21
+ _TS_LANG = tsjavascript.language()
22
+ _TSX_LANG = tsjavascript.language()
23
+ _TS_AVAILABLE = False
24
+
25
+ # Desteklenen dil eşlemesi
26
+ LANGUAGE_MAP: dict[str, tuple] = {
27
+ ".py": ("python", tspython.language()),
28
+ ".js": ("javascript", tsjavascript.language()),
29
+ ".jsx": ("javascript", tsjavascript.language()),
30
+ ".ts": ("typescript", _TS_LANG),
31
+ ".tsx": ("tsx", _TSX_LANG),
32
+ }
33
+
34
+
35
+ @dataclass
36
+ class FileAnalysisResult:
37
+ """Tek bir dosyanın analiz sonucu."""
38
+
39
+ file_path: str
40
+ ai_probability: float = 0.0
41
+ complexity_score: float = 0.0
42
+ comment_ratio: float = 0.0
43
+ avg_function_length: float = 0.0
44
+ single_commit_ratio: float = 0.0
45
+ total_lines: int = 0
46
+ function_count: int = 0
47
+ desteklenmiyor: bool = False
48
+ hata: Optional[str] = None
49
+
50
+ @property
51
+ def complexity_label(self) -> str:
52
+ """Karmaşıklık seviyesini metin olarak döndür."""
53
+ if self.complexity_score < 5:
54
+ return "Düşük"
55
+ elif self.complexity_score < 15:
56
+ return "Orta"
57
+ else:
58
+ return "Yüksek"
59
+
60
+ @property
61
+ def ai_color(self) -> str:
62
+ """AI olasılığına göre renk emojisi döndür."""
63
+ if self.ai_probability >= 0.7:
64
+ return "🔴"
65
+ elif self.ai_probability >= 0.4:
66
+ return "🟡"
67
+ else:
68
+ return "🟢"
69
+
70
+
71
+ def _build_parser(ext: str) -> Optional[Parser]:
72
+ """Dosya uzantısına göre tree-sitter parser oluştur."""
73
+ if ext not in LANGUAGE_MAP:
74
+ return None
75
+ _, lang_obj = LANGUAGE_MAP[ext]
76
+ language = Language(lang_obj)
77
+ parser = Parser(language)
78
+ return parser
79
+
80
+
81
+ def _count_lines(source: str) -> tuple[int, int]:
82
+ """Toplam satır ve yorum satırı sayısını döndür (toplam, yorum)."""
83
+ lines = source.splitlines()
84
+ toplam = len(lines)
85
+ yorum = 0
86
+ for line in lines:
87
+ stripped = line.strip()
88
+ # Python, JS, TS tek satır yorumları
89
+ if stripped.startswith("#") or stripped.startswith("//"):
90
+ yorum += 1
91
+ # Çok satırlı yorum içinde olup olmadığını basit regex ile yakala
92
+ elif stripped.startswith("*") or stripped.startswith("/*") or stripped.startswith('"""') or stripped.startswith("'''"):
93
+ yorum += 1
94
+ return toplam, yorum
95
+
96
+
97
+ def _collect_functions(node: Node, functions: list[Node]) -> None:
98
+ """Ağaç içindeki tüm fonksiyon düğümlerini özyinelemeli topla."""
99
+ fonksiyon_tipleri = {
100
+ "function_definition", # Python
101
+ "function_declaration", # JS/TS
102
+ "method_definition", # JS/TS class method
103
+ "method_signature", # TS interface method
104
+ "abstract_method_signature",# TS abstract
105
+ "arrow_function", # JS/TS arrow
106
+ "function_expression", # JS/TS
107
+ "generator_function", # JS/TS generator
108
+ "generator_function_declaration",
109
+ }
110
+ if node.type in fonksiyon_tipleri:
111
+ functions.append(node)
112
+ for child in node.children:
113
+ _collect_functions(child, functions)
114
+
115
+
116
+ def _calculate_cyclomatic_complexity(node: Node) -> float:
117
+ """
118
+ Basit cyclomatic complexity hesapla.
119
+ Karar noktalarını (if, for, while, case, &&, ||) say.
120
+ """
121
+ karar_tipleri = {
122
+ "if_statement", "elif_clause", "for_statement", "while_statement",
123
+ "with_statement", "try_statement", "except_clause",
124
+ "if_expression", # Python ternary
125
+ "switch_case", "case_clause",
126
+ # JS/TS
127
+ "if", "for", "while", "switch", "catch",
128
+ "ternary_expression",
129
+ "&&", "||", "??",
130
+ }
131
+ sayac = 1 # Temel yol
132
+
133
+ def _gez(n: Node) -> None:
134
+ nonlocal sayac
135
+ if n.type in karar_tipleri:
136
+ sayac += 1
137
+ # Mantıksal operatörler
138
+ if n.type in {"boolean_operator", "logical_expression"}:
139
+ sayac += 1
140
+ for child in n.children:
141
+ _gez(child)
142
+
143
+ _gez(node)
144
+ return float(sayac)
145
+
146
+
147
+ def analyze_file(
148
+ file_path: Path,
149
+ single_commit_ratio: float = 0.0,
150
+ ) -> FileAnalysisResult:
151
+ """
152
+ Dosyayı AST ile analiz et ve AI imza metriklerini hesapla.
153
+
154
+ Args:
155
+ file_path: Analiz edilecek dosyanın yolu
156
+ single_commit_ratio: Tek commit'te gelen satır oranı (dışarıdan verilir)
157
+
158
+ Returns:
159
+ FileAnalysisResult nesnesi
160
+ """
161
+ sonuc = FileAnalysisResult(
162
+ file_path=str(file_path),
163
+ single_commit_ratio=single_commit_ratio,
164
+ )
165
+
166
+ # Dosya okunabilir mi?
167
+ try:
168
+ kaynak = file_path.read_text(encoding="utf-8", errors="replace")
169
+ except Exception as e:
170
+ sonuc.hata = f"Dosya okunamadı: {e}"
171
+ return sonuc
172
+
173
+ ext = file_path.suffix.lower()
174
+ parser = _build_parser(ext)
175
+
176
+ if parser is None:
177
+ sonuc.desteklenmiyor = True
178
+ return sonuc
179
+
180
+ # Satır sayıları
181
+ toplam_satir, yorum_satir = _count_lines(kaynak)
182
+ sonuc.total_lines = toplam_satir
183
+ sonuc.comment_ratio = (yorum_satir / toplam_satir) if toplam_satir > 0 else 0.0
184
+
185
+ # AST parse
186
+ try:
187
+ tree = parser.parse(bytes(kaynak, "utf8"))
188
+ except Exception as e:
189
+ sonuc.hata = f"AST parse hatası: {e}"
190
+ return sonuc
191
+
192
+ # Fonksiyon analizi
193
+ fonksiyonlar: list[Node] = []
194
+ _collect_functions(tree.root_node, fonksiyonlar)
195
+ sonuc.function_count = len(fonksiyonlar)
196
+
197
+ if fonksiyonlar:
198
+ uzunluklar = [
199
+ f.end_point[0] - f.start_point[0] + 1
200
+ for f in fonksiyonlar
201
+ ]
202
+ sonuc.avg_function_length = sum(uzunluklar) / len(uzunluklar)
203
+ else:
204
+ # Fonksiyon yoksa toplam satırı tek blok say
205
+ sonuc.avg_function_length = float(toplam_satir)
206
+
207
+ # Cyclomatic complexity (tüm dosya üzerinden)
208
+ sonuc.complexity_score = _calculate_cyclomatic_complexity(tree.root_node)
209
+
210
+ # AI olasılığı hesapla
211
+ sonuc.ai_probability = _calculate_ai_probability(sonuc)
212
+
213
+ return sonuc
214
+
215
+
216
+ def _calculate_ai_probability(sonuc: FileAnalysisResult) -> float:
217
+ """
218
+ Kural tabanlı AI olasılığı skoru hesapla (0.0 – 1.0).
219
+
220
+ Kurallar:
221
+ - comment_ratio > 0.3 → +0.20
222
+ - avg_function_length > 50 → +0.15
223
+ - single_commit_ratio > 0.7 → +0.30
224
+ - complexity yüksek & tek commit → +0.25
225
+ """
226
+ skor = 0.0
227
+
228
+ # Kural 1: Aşırı yorum oranı (AI kodu genelde çok yorum yazar)
229
+ if sonuc.comment_ratio > 0.3:
230
+ skor += 0.20
231
+
232
+ # Kural 2: Uzun fonksiyonlar (AI genelde büyük bloklar üretir)
233
+ if sonuc.avg_function_length > 50:
234
+ skor += 0.15
235
+
236
+ # Kural 3: Tek commit'te büyük değişiklik (toplu yapıştırma işareti)
237
+ if sonuc.single_commit_ratio > 0.7:
238
+ skor += 0.30
239
+
240
+ # Kural 4: Yüksek karmaşıklık + tek seferlik commit
241
+ if sonuc.complexity_score > 10 and sonuc.single_commit_ratio > 0.5:
242
+ skor += 0.25
243
+
244
+ # 0.0 – 1.0 arasına normalize et
245
+ return min(skor, 1.0)