codedna 0.9.4__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.4 → codedna-0.9.6}/PKG-INFO +1 -3
- {codedna-0.9.4 → codedna-0.9.6}/codedna/__init__.py +1 -1
- {codedna-0.9.4 → codedna-0.9.6}/codedna/analyzer.py +115 -34
- {codedna-0.9.4 → codedna-0.9.6}/codedna/cli.py +32 -3
- {codedna-0.9.4 → codedna-0.9.6}/codedna/scorer.py +37 -1
- {codedna-0.9.4 → codedna-0.9.6}/pyproject.toml +1 -4
- {codedna-0.9.4 → codedna-0.9.6}/.gitignore +0 -0
- {codedna-0.9.4 → codedna-0.9.6}/LICENSE +0 -0
- {codedna-0.9.4 → codedna-0.9.6}/README.md +0 -0
- {codedna-0.9.4 → codedna-0.9.6}/codedna/ai.py +0 -0
- {codedna-0.9.4 → codedna-0.9.6}/codedna/ai_box.py +0 -0
- {codedna-0.9.4 → codedna-0.9.6}/codedna/ai_fingerprint.py +0 -0
- {codedna-0.9.4 → codedna-0.9.6}/codedna/api.py +0 -0
- {codedna-0.9.4 → codedna-0.9.6}/codedna/auth.py +0 -0
- {codedna-0.9.4 → codedna-0.9.6}/codedna/bus_factor.py +0 -0
- {codedna-0.9.4 → codedna-0.9.6}/codedna/db.py +0 -0
- {codedna-0.9.4 → codedna-0.9.6}/codedna/demo.py +0 -0
- {codedna-0.9.4 → codedna-0.9.6}/codedna/git_hook.py +0 -0
- {codedna-0.9.4 → codedna-0.9.6}/codedna/integrations/__init__.py +0 -0
- {codedna-0.9.4 → codedna-0.9.6}/codedna/integrations/github_bot.py +0 -0
- {codedna-0.9.4 → codedna-0.9.6}/codedna/integrations/jira.py +0 -0
- {codedna-0.9.4 → codedna-0.9.6}/codedna/integrations/lemonsqueezy.py +0 -0
- {codedna-0.9.4 → codedna-0.9.6}/codedna/interview.py +0 -0
- {codedna-0.9.4 → codedna-0.9.6}/codedna/natureco_account.py +0 -0
- {codedna-0.9.4 → codedna-0.9.6}/codedna/nl.py +0 -0
- {codedna-0.9.4 → codedna-0.9.6}/codedna/nl_tools.json +0 -0
- {codedna-0.9.4 → codedna-0.9.6}/codedna/onboarding.py +0 -0
- {codedna-0.9.4 → codedna-0.9.6}/codedna/plan.py +0 -0
- {codedna-0.9.4 → codedna-0.9.6}/codedna/protection.py +0 -0
- {codedna-0.9.4 → codedna-0.9.6}/codedna/rate_limit.py +0 -0
- {codedna-0.9.4 → codedna-0.9.6}/codedna/sprint_health.py +0 -0
- {codedna-0.9.4 → codedna-0.9.6}/codedna/survey.py +0 -0
- {codedna-0.9.4 → codedna-0.9.6}/codedna/tech_debt.py +0 -0
- {codedna-0.9.4 → codedna-0.9.6}/codedna/webhooks.py +0 -0
- {codedna-0.9.4 → codedna-0.9.6}/dashboard/.env.local.example +0 -0
- {codedna-0.9.4 → codedna-0.9.6}/dashboard/.gitignore +0 -0
- {codedna-0.9.4 → codedna-0.9.6}/dashboard/Dockerfile +0 -0
- {codedna-0.9.4 → codedna-0.9.6}/dashboard/README.md +0 -0
- {codedna-0.9.4 → codedna-0.9.6}/dashboard/app/ai-compare/page.tsx +0 -0
- {codedna-0.9.4 → codedna-0.9.6}/dashboard/app/api/auth/login/route.ts +0 -0
- {codedna-0.9.4 → codedna-0.9.6}/dashboard/app/api/auth/logout/route.ts +0 -0
- {codedna-0.9.4 → codedna-0.9.6}/dashboard/app/api/auth/me/route.ts +0 -0
- {codedna-0.9.4 → codedna-0.9.6}/dashboard/app/api/auth/register/route.ts +0 -0
- {codedna-0.9.4 → codedna-0.9.6}/dashboard/app/api/billing/checkout/route.ts +0 -0
- {codedna-0.9.4 → codedna-0.9.6}/dashboard/app/billing/page.tsx +0 -0
- {codedna-0.9.4 → codedna-0.9.6}/dashboard/app/bus-factor/page.tsx +0 -0
- {codedna-0.9.4 → codedna-0.9.6}/dashboard/app/commits/page.tsx +0 -0
- {codedna-0.9.4 → codedna-0.9.6}/dashboard/app/debt/page.tsx +0 -0
- {codedna-0.9.4 → codedna-0.9.6}/dashboard/app/favicon.ico +0 -0
- {codedna-0.9.4 → codedna-0.9.6}/dashboard/app/feedback/page.tsx +0 -0
- {codedna-0.9.4 → codedna-0.9.6}/dashboard/app/files/page.tsx +0 -0
- {codedna-0.9.4 → codedna-0.9.6}/dashboard/app/fonts/GeistMonoVF.woff +0 -0
- {codedna-0.9.4 → codedna-0.9.6}/dashboard/app/fonts/GeistVF.woff +0 -0
- {codedna-0.9.4 → codedna-0.9.6}/dashboard/app/globals.css +0 -0
- {codedna-0.9.4 → codedna-0.9.6}/dashboard/app/interview/page.tsx +0 -0
- {codedna-0.9.4 → codedna-0.9.6}/dashboard/app/layout.tsx +0 -0
- {codedna-0.9.4 → codedna-0.9.6}/dashboard/app/login/page.tsx +0 -0
- {codedna-0.9.4 → codedna-0.9.6}/dashboard/app/onboarding/page.tsx +0 -0
- {codedna-0.9.4 → codedna-0.9.6}/dashboard/app/page.tsx +0 -0
- {codedna-0.9.4 → codedna-0.9.6}/dashboard/app/pricing/page.tsx +0 -0
- {codedna-0.9.4 → codedna-0.9.6}/dashboard/app/protected/page.tsx +0 -0
- {codedna-0.9.4 → codedna-0.9.6}/dashboard/app/register/page.tsx +0 -0
- {codedna-0.9.4 → codedna-0.9.6}/dashboard/app/repos/page.tsx +0 -0
- {codedna-0.9.4 → codedna-0.9.6}/dashboard/app/settings/integrations/page.tsx +0 -0
- {codedna-0.9.4 → codedna-0.9.6}/dashboard/app/sprints/page.tsx +0 -0
- {codedna-0.9.4 → codedna-0.9.6}/dashboard/app/trends/page.tsx +0 -0
- {codedna-0.9.4 → codedna-0.9.6}/dashboard/components/AuthProvider.tsx +0 -0
- {codedna-0.9.4 → codedna-0.9.6}/dashboard/components/ClientProviders.tsx +0 -0
- {codedna-0.9.4 → codedna-0.9.6}/dashboard/components/CommitTable.tsx +0 -0
- {codedna-0.9.4 → codedna-0.9.6}/dashboard/components/CostInfoTooltip.tsx +0 -0
- {codedna-0.9.4 → codedna-0.9.6}/dashboard/components/ErrorBanner.tsx +0 -0
- {codedna-0.9.4 → codedna-0.9.6}/dashboard/components/FeatureGate.tsx +0 -0
- {codedna-0.9.4 → codedna-0.9.6}/dashboard/components/FileHeatmap.tsx +0 -0
- {codedna-0.9.4 → codedna-0.9.6}/dashboard/components/LanguageSwitcher.tsx +0 -0
- {codedna-0.9.4 → codedna-0.9.6}/dashboard/components/NavClient.tsx +0 -0
- {codedna-0.9.4 → codedna-0.9.6}/dashboard/components/OverviewTitles.tsx +0 -0
- {codedna-0.9.4 → codedna-0.9.6}/dashboard/components/QuickInsights.tsx +0 -0
- {codedna-0.9.4 → codedna-0.9.6}/dashboard/components/RiskBadge.tsx +0 -0
- {codedna-0.9.4 → codedna-0.9.6}/dashboard/components/SummaryCards.tsx +0 -0
- {codedna-0.9.4 → codedna-0.9.6}/dashboard/components/UnderstandingChart.tsx +0 -0
- {codedna-0.9.4 → codedna-0.9.6}/dashboard/components/UpgradeBanner.tsx +0 -0
- {codedna-0.9.4 → codedna-0.9.6}/dashboard/lib/api.ts +0 -0
- {codedna-0.9.4 → codedna-0.9.6}/dashboard/lib/auth.ts +0 -0
- {codedna-0.9.4 → codedna-0.9.6}/dashboard/lib/i18n/en.ts +0 -0
- {codedna-0.9.4 → codedna-0.9.6}/dashboard/lib/i18n/index.tsx +0 -0
- {codedna-0.9.4 → codedna-0.9.6}/dashboard/lib/i18n/tr.ts +0 -0
- {codedna-0.9.4 → codedna-0.9.6}/dashboard/lib/plan.ts +0 -0
- {codedna-0.9.4 → codedna-0.9.6}/dashboard/next-env.d.ts +0 -0
- {codedna-0.9.4 → codedna-0.9.6}/dashboard/next.config.mjs +0 -0
- {codedna-0.9.4 → codedna-0.9.6}/dashboard/package-lock.json +0 -0
- {codedna-0.9.4 → codedna-0.9.6}/dashboard/package.json +0 -0
- {codedna-0.9.4 → codedna-0.9.6}/dashboard/postcss.config.mjs +0 -0
- {codedna-0.9.4 → codedna-0.9.6}/dashboard/tailwind.config.ts +0 -0
- {codedna-0.9.4 → 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
|
|
@@ -41,8 +41,6 @@ Requires-Dist: tree-sitter-typescript>=0.23.2
|
|
|
41
41
|
Requires-Dist: tree-sitter>=0.25.2
|
|
42
42
|
Requires-Dist: typer>=0.26.7
|
|
43
43
|
Requires-Dist: uvicorn[standard]>=0.49.0
|
|
44
|
-
Provides-Extra: dashboard
|
|
45
|
-
Requires-Dist: next-cmd>=0.5.0; extra == 'dashboard'
|
|
46
44
|
Provides-Extra: dev
|
|
47
45
|
Requires-Dist: black>=26.5.1; extra == 'dev'
|
|
48
46
|
Requires-Dist: pytest-cov>=7.1.0; extra == 'dev'
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import math
|
|
5
6
|
import re
|
|
6
7
|
from dataclasses import dataclass, field
|
|
7
8
|
from pathlib import Path
|
|
@@ -75,6 +76,8 @@ class FileAnalysisResult:
|
|
|
75
76
|
comment_ratio: float = 0.0
|
|
76
77
|
avg_function_length: float = 0.0
|
|
77
78
|
single_commit_ratio: float = 0.0
|
|
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
|
|
78
81
|
total_lines: int = 0
|
|
79
82
|
function_count: int = 0
|
|
80
83
|
unsupported: bool = False # kept for internal use
|
|
@@ -243,8 +246,8 @@ def analyze_file(
|
|
|
243
246
|
# Cyclomatic complexity (whole file)
|
|
244
247
|
result.complexity_score = _calculate_cyclomatic_complexity(tree.root_node)
|
|
245
248
|
|
|
246
|
-
# Calculate AI probability
|
|
247
|
-
result.ai_probability = _calculate_ai_probability(result)
|
|
249
|
+
# Calculate AI probability (continuous + content-based)
|
|
250
|
+
result.ai_probability = _calculate_ai_probability(result, source)
|
|
248
251
|
|
|
249
252
|
# Generate explanation
|
|
250
253
|
result.explanation = explain_ai_score(result)
|
|
@@ -339,28 +342,35 @@ def explain_ai_score(result: FileAnalysisResult) -> list[str]:
|
|
|
339
342
|
"""
|
|
340
343
|
reasons: list[str] = []
|
|
341
344
|
|
|
345
|
+
if result.content_ai_signal >= 0.15:
|
|
346
|
+
pct = int(result.content_ai_signal * 100)
|
|
347
|
+
reasons.append(
|
|
348
|
+
f"Code carries AI-style patterns ({pct}% content signal) — e.g. type hints, "
|
|
349
|
+
f"defensive boilerplate, 'step' comments, emoji"
|
|
350
|
+
)
|
|
351
|
+
|
|
342
352
|
if result.comment_ratio > 0.3:
|
|
343
353
|
pct = int(result.comment_ratio * 100)
|
|
344
354
|
reasons.append(
|
|
345
|
-
f"High comment ratio ({pct}%) — AI-generated code tends to over-comment
|
|
355
|
+
f"High comment ratio ({pct}%) — AI-generated code tends to over-comment"
|
|
346
356
|
)
|
|
347
357
|
|
|
348
358
|
if result.avg_function_length > 50:
|
|
349
359
|
reasons.append(
|
|
350
360
|
f"Long avg. function length ({result.avg_function_length:.0f} lines) — "
|
|
351
|
-
f"AI tends to produce larger blocks
|
|
361
|
+
f"AI tends to produce larger blocks"
|
|
352
362
|
)
|
|
353
363
|
|
|
354
364
|
if result.single_commit_ratio > 0.7:
|
|
355
365
|
pct = int(result.single_commit_ratio * 100)
|
|
356
366
|
reasons.append(
|
|
357
|
-
f"High single-commit ratio ({pct}%) — bulk paste indicator
|
|
367
|
+
f"High single-commit ratio ({pct}%) — bulk paste indicator"
|
|
358
368
|
)
|
|
359
369
|
|
|
360
370
|
if result.complexity_score > 10 and result.single_commit_ratio > 0.5:
|
|
361
371
|
reasons.append(
|
|
362
372
|
f"High complexity ({result.complexity_score:.0f}) combined with bulk change — "
|
|
363
|
-
f"complex AI-generated code pattern
|
|
373
|
+
f"complex AI-generated code pattern"
|
|
364
374
|
)
|
|
365
375
|
|
|
366
376
|
if result.function_count == 0:
|
|
@@ -372,33 +382,104 @@ def explain_ai_score(result: FileAnalysisResult) -> list[str]:
|
|
|
372
382
|
return reasons
|
|
373
383
|
|
|
374
384
|
|
|
375
|
-
def
|
|
385
|
+
def _smoothstep(x: float, lo: float, hi: float) -> float:
|
|
386
|
+
"""Continuous 0→1 ramp between lo and hi (replaces hard thresholds so scores
|
|
387
|
+
are file-specific and granular instead of clustering on a few binned values)."""
|
|
388
|
+
if hi <= lo:
|
|
389
|
+
return 1.0 if x >= hi else 0.0
|
|
390
|
+
t = max(0.0, min(1.0, (x - lo) / (hi - lo)))
|
|
391
|
+
return t * t * (3.0 - 2.0 * t)
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
# Content-based AI-authorship patterns (regex, weight). These look at what the code
|
|
395
|
+
# actually *is* — the stylistic fingerprints AI assistants tend to leave — rather than
|
|
396
|
+
# only git history. Density of matches feeds the score directly.
|
|
397
|
+
_AI_CONTENT_PATTERNS: list[tuple[str, float]] = [
|
|
398
|
+
(r"#\s*(Step\s*\d|Initialize|Return the|Create the|Loop through|Iterate over|Handle the|Validate|Check if|Set up|Ensure|First,|Finally,)", 1.0),
|
|
399
|
+
(r"//\s*(Step\s*\d|Initialize|Return the|Create the|Handle the|Validate|Check if|Set up|Ensure)", 1.0),
|
|
400
|
+
(r'"""', 0.25), # docstrings (AI documents everything)
|
|
401
|
+
(r"->\s*[A-Za-z_\[\"']", 0.45), # return type hints
|
|
402
|
+
(r":\s*(str|int|float|bool|list|dict|tuple|Optional|Any|Dict|List|Union)\b", 0.35), # param type hints
|
|
403
|
+
(r"^\s*try:\s*$", 0.4), # defensive try/except everywhere
|
|
404
|
+
(r"if\s+\w+\s+is\s+None", 0.4), # explicit None guards
|
|
405
|
+
(r"except\s+Exception\b", 0.35), # broad catch-alls
|
|
406
|
+
(r"[\U0001F300-\U0001FAFF☀-➿←-⇿✓✗]", 0.6), # emoji/✓/✗/→ in code
|
|
407
|
+
(r"\b(helper|handler|wrapper|manager|processor|validator)\b", 0.2), # generic role names
|
|
408
|
+
(r"console\.(log|error)\(.*(✓|✗|→|✅|❌|🧬|⚠)", 0.5),
|
|
409
|
+
]
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def _content_ai_signal(source: str) -> float:
|
|
413
|
+
"""A content-based AI signal in [0,1], independent of git history: how strongly
|
|
414
|
+
the code carries the stylistic fingerprints AI assistants tend to leave."""
|
|
415
|
+
lines = source.splitlines()
|
|
416
|
+
n = max(len(lines), 1)
|
|
417
|
+
total = 0.0
|
|
418
|
+
for pattern, weight in _AI_CONTENT_PATTERNS:
|
|
419
|
+
try:
|
|
420
|
+
matches = sum(1 for line in lines if re.search(pattern, line))
|
|
421
|
+
except re.error:
|
|
422
|
+
continue
|
|
423
|
+
total += (matches / n) * weight
|
|
424
|
+
# Saturating density → [0,1]; small densities still register, high ones plateau.
|
|
425
|
+
return 1.0 - math.exp(-4.0 * total)
|
|
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?".
|
|
376
442
|
"""
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
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
|
+
|
|
458
|
+
def _calculate_ai_probability(result: FileAnalysisResult, source: str = "") -> float:
|
|
459
|
+
"""Continuous, content-aware AI probability score (0.0 – 1.0).
|
|
460
|
+
|
|
461
|
+
Combines graded structural signals (comments, function size, cyclomatic
|
|
462
|
+
complexity), git-history signals (bulk single-commit paste) and a
|
|
463
|
+
content-based fingerprint of AI code style. Unlike the previous four binary
|
|
464
|
+
rules, every input is continuous so scores reflect each file individually.
|
|
384
465
|
"""
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
#
|
|
392
|
-
if
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
#
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
return min(score, 1.0)
|
|
466
|
+
# Graded structural / history signals — smooth ramps, not on/off thresholds.
|
|
467
|
+
comment = _smoothstep(result.comment_ratio, 0.12, 0.45)
|
|
468
|
+
func_len = _smoothstep(result.avg_function_length, 25.0, 90.0)
|
|
469
|
+
bulk = _smoothstep(result.single_commit_ratio, 0.35, 0.90)
|
|
470
|
+
complexity = _smoothstep(result.complexity_score, 6.0, 25.0)
|
|
471
|
+
|
|
472
|
+
# Content fingerprint — the actual code style (strongest single signal).
|
|
473
|
+
content = _content_ai_signal(source) if source else 0.0
|
|
474
|
+
result.content_ai_signal = round(content, 4)
|
|
475
|
+
|
|
476
|
+
# Weighted blend (weights sum to 1.0). Content carries the most weight because
|
|
477
|
+
# it inspects what the code *is*, not just how it landed in git.
|
|
478
|
+
score = (
|
|
479
|
+
0.14 * comment
|
|
480
|
+
+ 0.12 * func_len
|
|
481
|
+
+ 0.20 * bulk
|
|
482
|
+
+ 0.12 * (complexity * bulk) # complexity matters most when bulk-committed
|
|
483
|
+
+ 0.42 * content
|
|
484
|
+
)
|
|
485
|
+
return round(min(max(score, 0.0), 1.0), 4)
|
|
@@ -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"
|
|
@@ -47,9 +47,6 @@ dependencies = [
|
|
|
47
47
|
]
|
|
48
48
|
|
|
49
49
|
[project.optional-dependencies]
|
|
50
|
-
dashboard = [
|
|
51
|
-
"next-cmd>=0.5.0", # Next.js process management
|
|
52
|
-
]
|
|
53
50
|
nl = [
|
|
54
51
|
"huggingface_hub",
|
|
55
52
|
"pyarrow>=14,<18",
|
|
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
|