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.
Files changed (94) hide show
  1. {codedna-0.9.4 → codedna-0.9.6}/PKG-INFO +1 -3
  2. {codedna-0.9.4 → codedna-0.9.6}/codedna/__init__.py +1 -1
  3. {codedna-0.9.4 → codedna-0.9.6}/codedna/analyzer.py +115 -34
  4. {codedna-0.9.4 → codedna-0.9.6}/codedna/cli.py +32 -3
  5. {codedna-0.9.4 → codedna-0.9.6}/codedna/scorer.py +37 -1
  6. {codedna-0.9.4 → codedna-0.9.6}/pyproject.toml +1 -4
  7. {codedna-0.9.4 → codedna-0.9.6}/.gitignore +0 -0
  8. {codedna-0.9.4 → codedna-0.9.6}/LICENSE +0 -0
  9. {codedna-0.9.4 → codedna-0.9.6}/README.md +0 -0
  10. {codedna-0.9.4 → codedna-0.9.6}/codedna/ai.py +0 -0
  11. {codedna-0.9.4 → codedna-0.9.6}/codedna/ai_box.py +0 -0
  12. {codedna-0.9.4 → codedna-0.9.6}/codedna/ai_fingerprint.py +0 -0
  13. {codedna-0.9.4 → codedna-0.9.6}/codedna/api.py +0 -0
  14. {codedna-0.9.4 → codedna-0.9.6}/codedna/auth.py +0 -0
  15. {codedna-0.9.4 → codedna-0.9.6}/codedna/bus_factor.py +0 -0
  16. {codedna-0.9.4 → codedna-0.9.6}/codedna/db.py +0 -0
  17. {codedna-0.9.4 → codedna-0.9.6}/codedna/demo.py +0 -0
  18. {codedna-0.9.4 → codedna-0.9.6}/codedna/git_hook.py +0 -0
  19. {codedna-0.9.4 → codedna-0.9.6}/codedna/integrations/__init__.py +0 -0
  20. {codedna-0.9.4 → codedna-0.9.6}/codedna/integrations/github_bot.py +0 -0
  21. {codedna-0.9.4 → codedna-0.9.6}/codedna/integrations/jira.py +0 -0
  22. {codedna-0.9.4 → codedna-0.9.6}/codedna/integrations/lemonsqueezy.py +0 -0
  23. {codedna-0.9.4 → codedna-0.9.6}/codedna/interview.py +0 -0
  24. {codedna-0.9.4 → codedna-0.9.6}/codedna/natureco_account.py +0 -0
  25. {codedna-0.9.4 → codedna-0.9.6}/codedna/nl.py +0 -0
  26. {codedna-0.9.4 → codedna-0.9.6}/codedna/nl_tools.json +0 -0
  27. {codedna-0.9.4 → codedna-0.9.6}/codedna/onboarding.py +0 -0
  28. {codedna-0.9.4 → codedna-0.9.6}/codedna/plan.py +0 -0
  29. {codedna-0.9.4 → codedna-0.9.6}/codedna/protection.py +0 -0
  30. {codedna-0.9.4 → codedna-0.9.6}/codedna/rate_limit.py +0 -0
  31. {codedna-0.9.4 → codedna-0.9.6}/codedna/sprint_health.py +0 -0
  32. {codedna-0.9.4 → codedna-0.9.6}/codedna/survey.py +0 -0
  33. {codedna-0.9.4 → codedna-0.9.6}/codedna/tech_debt.py +0 -0
  34. {codedna-0.9.4 → codedna-0.9.6}/codedna/webhooks.py +0 -0
  35. {codedna-0.9.4 → codedna-0.9.6}/dashboard/.env.local.example +0 -0
  36. {codedna-0.9.4 → codedna-0.9.6}/dashboard/.gitignore +0 -0
  37. {codedna-0.9.4 → codedna-0.9.6}/dashboard/Dockerfile +0 -0
  38. {codedna-0.9.4 → codedna-0.9.6}/dashboard/README.md +0 -0
  39. {codedna-0.9.4 → codedna-0.9.6}/dashboard/app/ai-compare/page.tsx +0 -0
  40. {codedna-0.9.4 → codedna-0.9.6}/dashboard/app/api/auth/login/route.ts +0 -0
  41. {codedna-0.9.4 → codedna-0.9.6}/dashboard/app/api/auth/logout/route.ts +0 -0
  42. {codedna-0.9.4 → codedna-0.9.6}/dashboard/app/api/auth/me/route.ts +0 -0
  43. {codedna-0.9.4 → codedna-0.9.6}/dashboard/app/api/auth/register/route.ts +0 -0
  44. {codedna-0.9.4 → codedna-0.9.6}/dashboard/app/api/billing/checkout/route.ts +0 -0
  45. {codedna-0.9.4 → codedna-0.9.6}/dashboard/app/billing/page.tsx +0 -0
  46. {codedna-0.9.4 → codedna-0.9.6}/dashboard/app/bus-factor/page.tsx +0 -0
  47. {codedna-0.9.4 → codedna-0.9.6}/dashboard/app/commits/page.tsx +0 -0
  48. {codedna-0.9.4 → codedna-0.9.6}/dashboard/app/debt/page.tsx +0 -0
  49. {codedna-0.9.4 → codedna-0.9.6}/dashboard/app/favicon.ico +0 -0
  50. {codedna-0.9.4 → codedna-0.9.6}/dashboard/app/feedback/page.tsx +0 -0
  51. {codedna-0.9.4 → codedna-0.9.6}/dashboard/app/files/page.tsx +0 -0
  52. {codedna-0.9.4 → codedna-0.9.6}/dashboard/app/fonts/GeistMonoVF.woff +0 -0
  53. {codedna-0.9.4 → codedna-0.9.6}/dashboard/app/fonts/GeistVF.woff +0 -0
  54. {codedna-0.9.4 → codedna-0.9.6}/dashboard/app/globals.css +0 -0
  55. {codedna-0.9.4 → codedna-0.9.6}/dashboard/app/interview/page.tsx +0 -0
  56. {codedna-0.9.4 → codedna-0.9.6}/dashboard/app/layout.tsx +0 -0
  57. {codedna-0.9.4 → codedna-0.9.6}/dashboard/app/login/page.tsx +0 -0
  58. {codedna-0.9.4 → codedna-0.9.6}/dashboard/app/onboarding/page.tsx +0 -0
  59. {codedna-0.9.4 → codedna-0.9.6}/dashboard/app/page.tsx +0 -0
  60. {codedna-0.9.4 → codedna-0.9.6}/dashboard/app/pricing/page.tsx +0 -0
  61. {codedna-0.9.4 → codedna-0.9.6}/dashboard/app/protected/page.tsx +0 -0
  62. {codedna-0.9.4 → codedna-0.9.6}/dashboard/app/register/page.tsx +0 -0
  63. {codedna-0.9.4 → codedna-0.9.6}/dashboard/app/repos/page.tsx +0 -0
  64. {codedna-0.9.4 → codedna-0.9.6}/dashboard/app/settings/integrations/page.tsx +0 -0
  65. {codedna-0.9.4 → codedna-0.9.6}/dashboard/app/sprints/page.tsx +0 -0
  66. {codedna-0.9.4 → codedna-0.9.6}/dashboard/app/trends/page.tsx +0 -0
  67. {codedna-0.9.4 → codedna-0.9.6}/dashboard/components/AuthProvider.tsx +0 -0
  68. {codedna-0.9.4 → codedna-0.9.6}/dashboard/components/ClientProviders.tsx +0 -0
  69. {codedna-0.9.4 → codedna-0.9.6}/dashboard/components/CommitTable.tsx +0 -0
  70. {codedna-0.9.4 → codedna-0.9.6}/dashboard/components/CostInfoTooltip.tsx +0 -0
  71. {codedna-0.9.4 → codedna-0.9.6}/dashboard/components/ErrorBanner.tsx +0 -0
  72. {codedna-0.9.4 → codedna-0.9.6}/dashboard/components/FeatureGate.tsx +0 -0
  73. {codedna-0.9.4 → codedna-0.9.6}/dashboard/components/FileHeatmap.tsx +0 -0
  74. {codedna-0.9.4 → codedna-0.9.6}/dashboard/components/LanguageSwitcher.tsx +0 -0
  75. {codedna-0.9.4 → codedna-0.9.6}/dashboard/components/NavClient.tsx +0 -0
  76. {codedna-0.9.4 → codedna-0.9.6}/dashboard/components/OverviewTitles.tsx +0 -0
  77. {codedna-0.9.4 → codedna-0.9.6}/dashboard/components/QuickInsights.tsx +0 -0
  78. {codedna-0.9.4 → codedna-0.9.6}/dashboard/components/RiskBadge.tsx +0 -0
  79. {codedna-0.9.4 → codedna-0.9.6}/dashboard/components/SummaryCards.tsx +0 -0
  80. {codedna-0.9.4 → codedna-0.9.6}/dashboard/components/UnderstandingChart.tsx +0 -0
  81. {codedna-0.9.4 → codedna-0.9.6}/dashboard/components/UpgradeBanner.tsx +0 -0
  82. {codedna-0.9.4 → codedna-0.9.6}/dashboard/lib/api.ts +0 -0
  83. {codedna-0.9.4 → codedna-0.9.6}/dashboard/lib/auth.ts +0 -0
  84. {codedna-0.9.4 → codedna-0.9.6}/dashboard/lib/i18n/en.ts +0 -0
  85. {codedna-0.9.4 → codedna-0.9.6}/dashboard/lib/i18n/index.tsx +0 -0
  86. {codedna-0.9.4 → codedna-0.9.6}/dashboard/lib/i18n/tr.ts +0 -0
  87. {codedna-0.9.4 → codedna-0.9.6}/dashboard/lib/plan.ts +0 -0
  88. {codedna-0.9.4 → codedna-0.9.6}/dashboard/next-env.d.ts +0 -0
  89. {codedna-0.9.4 → codedna-0.9.6}/dashboard/next.config.mjs +0 -0
  90. {codedna-0.9.4 → codedna-0.9.6}/dashboard/package-lock.json +0 -0
  91. {codedna-0.9.4 → codedna-0.9.6}/dashboard/package.json +0 -0
  92. {codedna-0.9.4 → codedna-0.9.6}/dashboard/postcss.config.mjs +0 -0
  93. {codedna-0.9.4 → codedna-0.9.6}/dashboard/tailwind.config.ts +0 -0
  94. {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.4
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'
@@ -1,4 +1,4 @@
1
1
  """CodeDNA — AI Code Transparency Tool."""
2
2
 
3
- __version__ = "0.9.4"
3
+ __version__ = "0.9.6"
4
4
  __app_name__ = "codedna"
@@ -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 (+0.20)"
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 (+0.15)"
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 (+0.30)"
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 (+0.25)"
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 _calculate_ai_probability(result: FileAnalysisResult) -> float:
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
- Calculate a rule-based AI probability score (0.0 – 1.0).
378
-
379
- Rules:
380
- - comment_ratio > 0.3 → +0.20 (AI tends to over-comment)
381
- - avg_function_length > 50 → +0.15 (AI tends to produce large blocks)
382
- - single_commit_ratio > 0.7+0.30 (bulk paste indicator)
383
- - high complexity + single commit+0.25
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 # complexharder 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
- score = 0.0
386
-
387
- # Rule 1: Excessive comment ratio (AI code tends to over-comment)
388
- if result.comment_ratio > 0.3:
389
- score += 0.20
390
-
391
- # Rule 2: Long functions (AI tends to produce large blocks)
392
- if result.avg_function_length > 50:
393
- score += 0.15
394
-
395
- # Rule 3: Large change in a single commit (bulk paste indicator)
396
- if result.single_commit_ratio > 0.7:
397
- score += 0.30
398
-
399
- # Rule 4: High complexity + single-commit
400
- if result.complexity_score > 10 and result.single_commit_ratio > 0.5:
401
- score += 0.25
402
-
403
- # Normalize to 0.0 – 1.0
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
- # Read understanding score from the single-query map
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.4"
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