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/__init__.py
ADDED
|
@@ -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)
|