codedna 0.9.5__tar.gz → 0.9.6__tar.gz
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-0.9.5 → codedna-0.9.6}/PKG-INFO +1 -1
- {codedna-0.9.5 → codedna-0.9.6}/codedna/__init__.py +1 -1
- {codedna-0.9.5 → codedna-0.9.6}/codedna/analyzer.py +31 -0
- {codedna-0.9.5 → codedna-0.9.6}/codedna/cli.py +32 -3
- {codedna-0.9.5 → codedna-0.9.6}/codedna/scorer.py +37 -1
- {codedna-0.9.5 → codedna-0.9.6}/pyproject.toml +1 -1
- {codedna-0.9.5 → codedna-0.9.6}/.gitignore +0 -0
- {codedna-0.9.5 → codedna-0.9.6}/LICENSE +0 -0
- {codedna-0.9.5 → codedna-0.9.6}/README.md +0 -0
- {codedna-0.9.5 → codedna-0.9.6}/codedna/ai.py +0 -0
- {codedna-0.9.5 → codedna-0.9.6}/codedna/ai_box.py +0 -0
- {codedna-0.9.5 → codedna-0.9.6}/codedna/ai_fingerprint.py +0 -0
- {codedna-0.9.5 → codedna-0.9.6}/codedna/api.py +0 -0
- {codedna-0.9.5 → codedna-0.9.6}/codedna/auth.py +0 -0
- {codedna-0.9.5 → codedna-0.9.6}/codedna/bus_factor.py +0 -0
- {codedna-0.9.5 → codedna-0.9.6}/codedna/db.py +0 -0
- {codedna-0.9.5 → codedna-0.9.6}/codedna/demo.py +0 -0
- {codedna-0.9.5 → codedna-0.9.6}/codedna/git_hook.py +0 -0
- {codedna-0.9.5 → codedna-0.9.6}/codedna/integrations/__init__.py +0 -0
- {codedna-0.9.5 → codedna-0.9.6}/codedna/integrations/github_bot.py +0 -0
- {codedna-0.9.5 → codedna-0.9.6}/codedna/integrations/jira.py +0 -0
- {codedna-0.9.5 → codedna-0.9.6}/codedna/integrations/lemonsqueezy.py +0 -0
- {codedna-0.9.5 → codedna-0.9.6}/codedna/interview.py +0 -0
- {codedna-0.9.5 → codedna-0.9.6}/codedna/natureco_account.py +0 -0
- {codedna-0.9.5 → codedna-0.9.6}/codedna/nl.py +0 -0
- {codedna-0.9.5 → codedna-0.9.6}/codedna/nl_tools.json +0 -0
- {codedna-0.9.5 → codedna-0.9.6}/codedna/onboarding.py +0 -0
- {codedna-0.9.5 → codedna-0.9.6}/codedna/plan.py +0 -0
- {codedna-0.9.5 → codedna-0.9.6}/codedna/protection.py +0 -0
- {codedna-0.9.5 → codedna-0.9.6}/codedna/rate_limit.py +0 -0
- {codedna-0.9.5 → codedna-0.9.6}/codedna/sprint_health.py +0 -0
- {codedna-0.9.5 → codedna-0.9.6}/codedna/survey.py +0 -0
- {codedna-0.9.5 → codedna-0.9.6}/codedna/tech_debt.py +0 -0
- {codedna-0.9.5 → codedna-0.9.6}/codedna/webhooks.py +0 -0
- {codedna-0.9.5 → codedna-0.9.6}/dashboard/.env.local.example +0 -0
- {codedna-0.9.5 → codedna-0.9.6}/dashboard/.gitignore +0 -0
- {codedna-0.9.5 → codedna-0.9.6}/dashboard/Dockerfile +0 -0
- {codedna-0.9.5 → codedna-0.9.6}/dashboard/README.md +0 -0
- {codedna-0.9.5 → codedna-0.9.6}/dashboard/app/ai-compare/page.tsx +0 -0
- {codedna-0.9.5 → codedna-0.9.6}/dashboard/app/api/auth/login/route.ts +0 -0
- {codedna-0.9.5 → codedna-0.9.6}/dashboard/app/api/auth/logout/route.ts +0 -0
- {codedna-0.9.5 → codedna-0.9.6}/dashboard/app/api/auth/me/route.ts +0 -0
- {codedna-0.9.5 → codedna-0.9.6}/dashboard/app/api/auth/register/route.ts +0 -0
- {codedna-0.9.5 → codedna-0.9.6}/dashboard/app/api/billing/checkout/route.ts +0 -0
- {codedna-0.9.5 → codedna-0.9.6}/dashboard/app/billing/page.tsx +0 -0
- {codedna-0.9.5 → codedna-0.9.6}/dashboard/app/bus-factor/page.tsx +0 -0
- {codedna-0.9.5 → codedna-0.9.6}/dashboard/app/commits/page.tsx +0 -0
- {codedna-0.9.5 → codedna-0.9.6}/dashboard/app/debt/page.tsx +0 -0
- {codedna-0.9.5 → codedna-0.9.6}/dashboard/app/favicon.ico +0 -0
- {codedna-0.9.5 → codedna-0.9.6}/dashboard/app/feedback/page.tsx +0 -0
- {codedna-0.9.5 → codedna-0.9.6}/dashboard/app/files/page.tsx +0 -0
- {codedna-0.9.5 → codedna-0.9.6}/dashboard/app/fonts/GeistMonoVF.woff +0 -0
- {codedna-0.9.5 → codedna-0.9.6}/dashboard/app/fonts/GeistVF.woff +0 -0
- {codedna-0.9.5 → codedna-0.9.6}/dashboard/app/globals.css +0 -0
- {codedna-0.9.5 → codedna-0.9.6}/dashboard/app/interview/page.tsx +0 -0
- {codedna-0.9.5 → codedna-0.9.6}/dashboard/app/layout.tsx +0 -0
- {codedna-0.9.5 → codedna-0.9.6}/dashboard/app/login/page.tsx +0 -0
- {codedna-0.9.5 → codedna-0.9.6}/dashboard/app/onboarding/page.tsx +0 -0
- {codedna-0.9.5 → codedna-0.9.6}/dashboard/app/page.tsx +0 -0
- {codedna-0.9.5 → codedna-0.9.6}/dashboard/app/pricing/page.tsx +0 -0
- {codedna-0.9.5 → codedna-0.9.6}/dashboard/app/protected/page.tsx +0 -0
- {codedna-0.9.5 → codedna-0.9.6}/dashboard/app/register/page.tsx +0 -0
- {codedna-0.9.5 → codedna-0.9.6}/dashboard/app/repos/page.tsx +0 -0
- {codedna-0.9.5 → codedna-0.9.6}/dashboard/app/settings/integrations/page.tsx +0 -0
- {codedna-0.9.5 → codedna-0.9.6}/dashboard/app/sprints/page.tsx +0 -0
- {codedna-0.9.5 → codedna-0.9.6}/dashboard/app/trends/page.tsx +0 -0
- {codedna-0.9.5 → codedna-0.9.6}/dashboard/components/AuthProvider.tsx +0 -0
- {codedna-0.9.5 → codedna-0.9.6}/dashboard/components/ClientProviders.tsx +0 -0
- {codedna-0.9.5 → codedna-0.9.6}/dashboard/components/CommitTable.tsx +0 -0
- {codedna-0.9.5 → codedna-0.9.6}/dashboard/components/CostInfoTooltip.tsx +0 -0
- {codedna-0.9.5 → codedna-0.9.6}/dashboard/components/ErrorBanner.tsx +0 -0
- {codedna-0.9.5 → codedna-0.9.6}/dashboard/components/FeatureGate.tsx +0 -0
- {codedna-0.9.5 → codedna-0.9.6}/dashboard/components/FileHeatmap.tsx +0 -0
- {codedna-0.9.5 → codedna-0.9.6}/dashboard/components/LanguageSwitcher.tsx +0 -0
- {codedna-0.9.5 → codedna-0.9.6}/dashboard/components/NavClient.tsx +0 -0
- {codedna-0.9.5 → codedna-0.9.6}/dashboard/components/OverviewTitles.tsx +0 -0
- {codedna-0.9.5 → codedna-0.9.6}/dashboard/components/QuickInsights.tsx +0 -0
- {codedna-0.9.5 → codedna-0.9.6}/dashboard/components/RiskBadge.tsx +0 -0
- {codedna-0.9.5 → codedna-0.9.6}/dashboard/components/SummaryCards.tsx +0 -0
- {codedna-0.9.5 → codedna-0.9.6}/dashboard/components/UnderstandingChart.tsx +0 -0
- {codedna-0.9.5 → codedna-0.9.6}/dashboard/components/UpgradeBanner.tsx +0 -0
- {codedna-0.9.5 → codedna-0.9.6}/dashboard/lib/api.ts +0 -0
- {codedna-0.9.5 → codedna-0.9.6}/dashboard/lib/auth.ts +0 -0
- {codedna-0.9.5 → codedna-0.9.6}/dashboard/lib/i18n/en.ts +0 -0
- {codedna-0.9.5 → codedna-0.9.6}/dashboard/lib/i18n/index.tsx +0 -0
- {codedna-0.9.5 → codedna-0.9.6}/dashboard/lib/i18n/tr.ts +0 -0
- {codedna-0.9.5 → codedna-0.9.6}/dashboard/lib/plan.ts +0 -0
- {codedna-0.9.5 → codedna-0.9.6}/dashboard/next-env.d.ts +0 -0
- {codedna-0.9.5 → codedna-0.9.6}/dashboard/next.config.mjs +0 -0
- {codedna-0.9.5 → codedna-0.9.6}/dashboard/package-lock.json +0 -0
- {codedna-0.9.5 → codedna-0.9.6}/dashboard/package.json +0 -0
- {codedna-0.9.5 → codedna-0.9.6}/dashboard/postcss.config.mjs +0 -0
- {codedna-0.9.5 → codedna-0.9.6}/dashboard/tailwind.config.ts +0 -0
- {codedna-0.9.5 → codedna-0.9.6}/dashboard/tsconfig.json +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codedna
|
|
3
|
-
Version: 0.9.
|
|
3
|
+
Version: 0.9.6
|
|
4
4
|
Summary: AI Code Transparency Tool — detect AI-written code, analyze commit quality, track understanding debt, sprint health, bus factor, and more
|
|
5
5
|
Project-URL: Homepage, https://codedna.dev
|
|
6
6
|
Project-URL: Repository, https://github.com/natureco-official/codedna
|
|
@@ -77,6 +77,7 @@ class FileAnalysisResult:
|
|
|
77
77
|
avg_function_length: float = 0.0
|
|
78
78
|
single_commit_ratio: float = 0.0
|
|
79
79
|
content_ai_signal: float = 0.0 # content-based AI signal (code patterns, 0–1)
|
|
80
|
+
understanding_estimate: float = 0.0 # auto-estimated team understanding (1–5), 0 = not computed
|
|
80
81
|
total_lines: int = 0
|
|
81
82
|
function_count: int = 0
|
|
82
83
|
unsupported: bool = False # kept for internal use
|
|
@@ -424,6 +425,36 @@ def _content_ai_signal(source: str) -> float:
|
|
|
424
425
|
return 1.0 - math.exp(-4.0 * total)
|
|
425
426
|
|
|
426
427
|
|
|
428
|
+
def estimate_understanding(
|
|
429
|
+
ai_probability: float,
|
|
430
|
+
complexity_score: float,
|
|
431
|
+
author_count: int,
|
|
432
|
+
days_since_last: float,
|
|
433
|
+
top_author_share: float,
|
|
434
|
+
) -> float:
|
|
435
|
+
"""Auto-estimate how well a file is likely understood by the team (1.0–5.0,
|
|
436
|
+
higher = better understood) — no manual survey required.
|
|
437
|
+
|
|
438
|
+
The 'understanding debt' insight: code that is AI-heavy, complex, stale and
|
|
439
|
+
owned by a single person is code nobody really understands — i.e. real risk.
|
|
440
|
+
This is what sets CodeDNA apart from plain "% AI" detectors: it answers
|
|
441
|
+
"AI wrote it, but does anyone on the team actually understand it?".
|
|
442
|
+
"""
|
|
443
|
+
complexity_norm = _smoothstep(complexity_score, 6.0, 30.0)
|
|
444
|
+
staleness = _smoothstep(days_since_last, 30.0, 365.0) # ramps over a month → a year
|
|
445
|
+
solo = top_author_share >= 0.85 or author_count <= 1
|
|
446
|
+
|
|
447
|
+
score = 5.0
|
|
448
|
+
score -= 2.3 * ai_probability # AI-heavy → likely unreviewed / not internalized
|
|
449
|
+
score -= 1.0 * complexity_norm # complex → harder to hold in one's head
|
|
450
|
+
score -= 1.3 * staleness # untouched for ages → knowledge decays
|
|
451
|
+
if solo:
|
|
452
|
+
score -= 0.7 * (0.4 + 0.6 * staleness) # bus-factor-of-1; worse when also stale
|
|
453
|
+
if author_count >= 3:
|
|
454
|
+
score += 0.4 # shared across the team → better understood
|
|
455
|
+
return round(max(1.0, min(5.0, score)), 2)
|
|
456
|
+
|
|
457
|
+
|
|
427
458
|
def _calculate_ai_probability(result: FileAnalysisResult, source: str = "") -> float:
|
|
428
459
|
"""Continuous, content-aware AI probability score (0.0 – 1.0).
|
|
429
460
|
|
|
@@ -244,7 +244,8 @@ def scan(
|
|
|
244
244
|
"ai_probability": round(s.ai_probability, 4),
|
|
245
245
|
"complexity": s.complexity_label,
|
|
246
246
|
"lines": s.total_lines,
|
|
247
|
-
"understanding": understanding_scores_map.get(s.file_path),
|
|
247
|
+
"understanding": understanding_scores_map.get(s.file_path), # survey-confirmed (1–5) or null
|
|
248
|
+
"understanding_estimate": s.understanding_estimate or None, # auto-estimated (1–5)
|
|
248
249
|
}
|
|
249
250
|
for s in results
|
|
250
251
|
],
|
|
@@ -277,11 +278,15 @@ def scan(
|
|
|
277
278
|
else:
|
|
278
279
|
complexity = "[green]Low[/green]"
|
|
279
280
|
|
|
280
|
-
#
|
|
281
|
+
# Understanding: survey-confirmed score if we have one, else the auto-estimate.
|
|
281
282
|
understanding_score = understanding_scores_map.get(s.file_path)
|
|
282
283
|
if understanding_score is not None:
|
|
283
284
|
color = "green" if understanding_score >= 4.0 else "yellow" if understanding_score >= 2.5 else "red"
|
|
284
285
|
understanding = f"[{color}]✅ {understanding_score:.1f}/5[/{color}]"
|
|
286
|
+
elif s.understanding_estimate:
|
|
287
|
+
est = s.understanding_estimate
|
|
288
|
+
color = "green" if est >= 4.0 else "yellow" if est >= 2.5 else "red"
|
|
289
|
+
understanding = f"[{color}]~{est:.1f}/5[/{color}]" # ~ = auto-estimated
|
|
285
290
|
else:
|
|
286
291
|
understanding = "[dim]⚠️ Unknown[/dim]"
|
|
287
292
|
|
|
@@ -296,12 +301,36 @@ def scan(
|
|
|
296
301
|
avg_ai = (total_ai / len(results)) * 100 if results else 0
|
|
297
302
|
risk_label, risk_color = _risk_label(avg_ai)
|
|
298
303
|
|
|
304
|
+
est_scores = [s.understanding_estimate for s in results if s.understanding_estimate]
|
|
305
|
+
avg_understanding = (sum(est_scores) / len(est_scores)) if est_scores else None
|
|
306
|
+
understanding_txt = (
|
|
307
|
+
f" · Avg. understanding: [bold]{avg_understanding:.1f}/5[/bold]" if avg_understanding is not None else ""
|
|
308
|
+
)
|
|
299
309
|
console.print(
|
|
300
310
|
f"\n[bold]Repo Summary:[/bold] {len(results)} files scanned · "
|
|
301
|
-
f"Avg. AI probability: [bold]{avg_ai:.0f}[/bold] · "
|
|
311
|
+
f"Avg. AI probability: [bold]{avg_ai:.0f}[/bold]{understanding_txt} · "
|
|
302
312
|
f"Risk: [bold {risk_color}]{risk_label}[/bold {risk_color}]\n"
|
|
303
313
|
)
|
|
304
314
|
|
|
315
|
+
# Understanding debt — CodeDNA's differentiator: AI-heavy code the team likely
|
|
316
|
+
# doesn't understand (auto-estimated from git ownership, recency, AI, complexity).
|
|
317
|
+
debt = [
|
|
318
|
+
s for s in results
|
|
319
|
+
if s.understanding_estimate and s.understanding_estimate < 3.0 and s.ai_probability >= 0.3
|
|
320
|
+
]
|
|
321
|
+
if debt:
|
|
322
|
+
debt.sort(key=lambda s: (s.understanding_estimate, -s.ai_probability))
|
|
323
|
+
console.print("[bold]🧠 Understanding Debt[/bold] [dim](AI-heavy code the team likely doesn't understand)[/dim]")
|
|
324
|
+
for s in debt[:5]:
|
|
325
|
+
rel_path = _shorten_path(s.file_path, str(root))
|
|
326
|
+
console.print(
|
|
327
|
+
f" [red]~{s.understanding_estimate:.1f}/5[/red] {rel_path} "
|
|
328
|
+
f"[dim](AI %{s.ai_probability * 100:.0f})[/dim]"
|
|
329
|
+
)
|
|
330
|
+
if len(debt) > 5:
|
|
331
|
+
console.print(f" [dim]... and {len(debt) - 5} more[/dim]")
|
|
332
|
+
console.print()
|
|
333
|
+
|
|
305
334
|
# AI explanations for high-risk files
|
|
306
335
|
high_risk = [s for s in results if s.ai_probability >= 0.4]
|
|
307
336
|
if high_risk and len(high_risk) <= 5:
|
|
@@ -8,7 +8,7 @@ from typing import Optional
|
|
|
8
8
|
from git import InvalidGitRepositoryError, Repo
|
|
9
9
|
from rich.console import Console
|
|
10
10
|
|
|
11
|
-
from codedna.analyzer import FileAnalysisResult, analyze_file
|
|
11
|
+
from codedna.analyzer import FileAnalysisResult, analyze_file, estimate_understanding
|
|
12
12
|
|
|
13
13
|
console = Console()
|
|
14
14
|
|
|
@@ -79,6 +79,32 @@ def calculate_single_commit_ratio(repo: Repo, file_path: str) -> float:
|
|
|
79
79
|
return 0.0
|
|
80
80
|
|
|
81
81
|
|
|
82
|
+
def file_git_signals(repo: Repo, file_path: str) -> dict:
|
|
83
|
+
"""Lightweight per-file git signals for the understanding estimate:
|
|
84
|
+
distinct author count, days since the last commit, and the top author's
|
|
85
|
+
share of commits (ownership concentration / bus-factor proxy)."""
|
|
86
|
+
import time
|
|
87
|
+
|
|
88
|
+
fallback = {"author_count": 0, "days_since_last": 9999.0, "top_author_share": 1.0}
|
|
89
|
+
try:
|
|
90
|
+
commits = list(repo.iter_commits(paths=file_path, max_count=100))
|
|
91
|
+
if not commits:
|
|
92
|
+
return fallback
|
|
93
|
+
authors: dict[str, int] = {}
|
|
94
|
+
for c in commits:
|
|
95
|
+
name = c.author.name or c.author.email or "unknown"
|
|
96
|
+
authors[name] = authors.get(name, 0) + 1
|
|
97
|
+
total = sum(authors.values()) or 1
|
|
98
|
+
last_ts = commits[0].committed_date # iter_commits yields newest first
|
|
99
|
+
return {
|
|
100
|
+
"author_count": len(authors),
|
|
101
|
+
"days_since_last": max(0.0, (time.time() - last_ts) / 86400.0),
|
|
102
|
+
"top_author_share": max(authors.values()) / total,
|
|
103
|
+
}
|
|
104
|
+
except Exception:
|
|
105
|
+
return fallback
|
|
106
|
+
|
|
107
|
+
|
|
82
108
|
def scan_repository(
|
|
83
109
|
repo_path: Optional[Path] = None,
|
|
84
110
|
max_files: int = 200,
|
|
@@ -160,6 +186,16 @@ def scan_repository(
|
|
|
160
186
|
|
|
161
187
|
result = analyze_file(file, single_commit_ratio=single_commit_ratio)
|
|
162
188
|
if not result.unsupported and result.error is None:
|
|
189
|
+
# Auto-estimate team understanding from git ownership + recency + AI/complexity
|
|
190
|
+
if repo and relative_path in tracked_files:
|
|
191
|
+
sig = file_git_signals(repo, relative_path)
|
|
192
|
+
result.understanding_estimate = estimate_understanding(
|
|
193
|
+
result.ai_probability,
|
|
194
|
+
result.complexity_score,
|
|
195
|
+
sig["author_count"],
|
|
196
|
+
sig["days_since_last"],
|
|
197
|
+
sig["top_author_share"],
|
|
198
|
+
)
|
|
163
199
|
results.append(result)
|
|
164
200
|
count += 1
|
|
165
201
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "codedna"
|
|
3
|
-
version = "0.9.
|
|
3
|
+
version = "0.9.6"
|
|
4
4
|
description = "AI Code Transparency Tool — detect AI-written code, analyze commit quality, track understanding debt, sprint health, bus factor, and more"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.10"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|