codedna 0.9.1__tar.gz → 0.9.2__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 (93) hide show
  1. {codedna-0.9.1 → codedna-0.9.2}/PKG-INFO +8 -5
  2. {codedna-0.9.1 → codedna-0.9.2}/codedna/analyzer.py +0 -2
  3. {codedna-0.9.1 → codedna-0.9.2}/codedna/api.py +1 -1
  4. {codedna-0.9.1 → codedna-0.9.2}/codedna/cli.py +23 -8
  5. {codedna-0.9.1 → codedna-0.9.2}/codedna/db.py +11 -5
  6. codedna-0.9.2/codedna/nl.py +234 -0
  7. codedna-0.9.2/codedna/nl_tools.json +127 -0
  8. {codedna-0.9.1 → codedna-0.9.2}/codedna/scorer.py +2 -1
  9. {codedna-0.9.1 → codedna-0.9.2}/codedna/tech_debt.py +1 -1
  10. {codedna-0.9.1 → codedna-0.9.2}/pyproject.toml +13 -5
  11. {codedna-0.9.1 → codedna-0.9.2}/.gitignore +0 -0
  12. {codedna-0.9.1 → codedna-0.9.2}/LICENSE +0 -0
  13. {codedna-0.9.1 → codedna-0.9.2}/README.md +0 -0
  14. {codedna-0.9.1 → codedna-0.9.2}/codedna/__init__.py +0 -0
  15. {codedna-0.9.1 → codedna-0.9.2}/codedna/ai.py +0 -0
  16. {codedna-0.9.1 → codedna-0.9.2}/codedna/ai_box.py +0 -0
  17. {codedna-0.9.1 → codedna-0.9.2}/codedna/ai_fingerprint.py +0 -0
  18. {codedna-0.9.1 → codedna-0.9.2}/codedna/auth.py +0 -0
  19. {codedna-0.9.1 → codedna-0.9.2}/codedna/bus_factor.py +0 -0
  20. {codedna-0.9.1 → codedna-0.9.2}/codedna/demo.py +0 -0
  21. {codedna-0.9.1 → codedna-0.9.2}/codedna/git_hook.py +0 -0
  22. {codedna-0.9.1 → codedna-0.9.2}/codedna/integrations/__init__.py +0 -0
  23. {codedna-0.9.1 → codedna-0.9.2}/codedna/integrations/github_bot.py +0 -0
  24. {codedna-0.9.1 → codedna-0.9.2}/codedna/integrations/jira.py +0 -0
  25. {codedna-0.9.1 → codedna-0.9.2}/codedna/integrations/lemonsqueezy.py +0 -0
  26. {codedna-0.9.1 → codedna-0.9.2}/codedna/interview.py +0 -0
  27. {codedna-0.9.1 → codedna-0.9.2}/codedna/onboarding.py +0 -0
  28. {codedna-0.9.1 → codedna-0.9.2}/codedna/plan.py +0 -0
  29. {codedna-0.9.1 → codedna-0.9.2}/codedna/protection.py +0 -0
  30. {codedna-0.9.1 → codedna-0.9.2}/codedna/rate_limit.py +0 -0
  31. {codedna-0.9.1 → codedna-0.9.2}/codedna/sprint_health.py +0 -0
  32. {codedna-0.9.1 → codedna-0.9.2}/codedna/survey.py +0 -0
  33. {codedna-0.9.1 → codedna-0.9.2}/codedna/webhooks.py +0 -0
  34. {codedna-0.9.1 → codedna-0.9.2}/dashboard/.env.local.example +0 -0
  35. {codedna-0.9.1 → codedna-0.9.2}/dashboard/.gitignore +0 -0
  36. {codedna-0.9.1 → codedna-0.9.2}/dashboard/Dockerfile +0 -0
  37. {codedna-0.9.1 → codedna-0.9.2}/dashboard/README.md +0 -0
  38. {codedna-0.9.1 → codedna-0.9.2}/dashboard/app/ai-compare/page.tsx +0 -0
  39. {codedna-0.9.1 → codedna-0.9.2}/dashboard/app/api/auth/login/route.ts +0 -0
  40. {codedna-0.9.1 → codedna-0.9.2}/dashboard/app/api/auth/logout/route.ts +0 -0
  41. {codedna-0.9.1 → codedna-0.9.2}/dashboard/app/api/auth/me/route.ts +0 -0
  42. {codedna-0.9.1 → codedna-0.9.2}/dashboard/app/api/auth/register/route.ts +0 -0
  43. {codedna-0.9.1 → codedna-0.9.2}/dashboard/app/api/billing/checkout/route.ts +0 -0
  44. {codedna-0.9.1 → codedna-0.9.2}/dashboard/app/billing/page.tsx +0 -0
  45. {codedna-0.9.1 → codedna-0.9.2}/dashboard/app/bus-factor/page.tsx +0 -0
  46. {codedna-0.9.1 → codedna-0.9.2}/dashboard/app/commits/page.tsx +0 -0
  47. {codedna-0.9.1 → codedna-0.9.2}/dashboard/app/debt/page.tsx +0 -0
  48. {codedna-0.9.1 → codedna-0.9.2}/dashboard/app/favicon.ico +0 -0
  49. {codedna-0.9.1 → codedna-0.9.2}/dashboard/app/feedback/page.tsx +0 -0
  50. {codedna-0.9.1 → codedna-0.9.2}/dashboard/app/files/page.tsx +0 -0
  51. {codedna-0.9.1 → codedna-0.9.2}/dashboard/app/fonts/GeistMonoVF.woff +0 -0
  52. {codedna-0.9.1 → codedna-0.9.2}/dashboard/app/fonts/GeistVF.woff +0 -0
  53. {codedna-0.9.1 → codedna-0.9.2}/dashboard/app/globals.css +0 -0
  54. {codedna-0.9.1 → codedna-0.9.2}/dashboard/app/interview/page.tsx +0 -0
  55. {codedna-0.9.1 → codedna-0.9.2}/dashboard/app/layout.tsx +0 -0
  56. {codedna-0.9.1 → codedna-0.9.2}/dashboard/app/login/page.tsx +0 -0
  57. {codedna-0.9.1 → codedna-0.9.2}/dashboard/app/onboarding/page.tsx +0 -0
  58. {codedna-0.9.1 → codedna-0.9.2}/dashboard/app/page.tsx +0 -0
  59. {codedna-0.9.1 → codedna-0.9.2}/dashboard/app/pricing/page.tsx +0 -0
  60. {codedna-0.9.1 → codedna-0.9.2}/dashboard/app/protected/page.tsx +0 -0
  61. {codedna-0.9.1 → codedna-0.9.2}/dashboard/app/register/page.tsx +0 -0
  62. {codedna-0.9.1 → codedna-0.9.2}/dashboard/app/repos/page.tsx +0 -0
  63. {codedna-0.9.1 → codedna-0.9.2}/dashboard/app/settings/integrations/page.tsx +0 -0
  64. {codedna-0.9.1 → codedna-0.9.2}/dashboard/app/sprints/page.tsx +0 -0
  65. {codedna-0.9.1 → codedna-0.9.2}/dashboard/app/trends/page.tsx +0 -0
  66. {codedna-0.9.1 → codedna-0.9.2}/dashboard/components/AuthProvider.tsx +0 -0
  67. {codedna-0.9.1 → codedna-0.9.2}/dashboard/components/ClientProviders.tsx +0 -0
  68. {codedna-0.9.1 → codedna-0.9.2}/dashboard/components/CommitTable.tsx +0 -0
  69. {codedna-0.9.1 → codedna-0.9.2}/dashboard/components/CostInfoTooltip.tsx +0 -0
  70. {codedna-0.9.1 → codedna-0.9.2}/dashboard/components/ErrorBanner.tsx +0 -0
  71. {codedna-0.9.1 → codedna-0.9.2}/dashboard/components/FeatureGate.tsx +0 -0
  72. {codedna-0.9.1 → codedna-0.9.2}/dashboard/components/FileHeatmap.tsx +0 -0
  73. {codedna-0.9.1 → codedna-0.9.2}/dashboard/components/LanguageSwitcher.tsx +0 -0
  74. {codedna-0.9.1 → codedna-0.9.2}/dashboard/components/NavClient.tsx +0 -0
  75. {codedna-0.9.1 → codedna-0.9.2}/dashboard/components/OverviewTitles.tsx +0 -0
  76. {codedna-0.9.1 → codedna-0.9.2}/dashboard/components/QuickInsights.tsx +0 -0
  77. {codedna-0.9.1 → codedna-0.9.2}/dashboard/components/RiskBadge.tsx +0 -0
  78. {codedna-0.9.1 → codedna-0.9.2}/dashboard/components/SummaryCards.tsx +0 -0
  79. {codedna-0.9.1 → codedna-0.9.2}/dashboard/components/UnderstandingChart.tsx +0 -0
  80. {codedna-0.9.1 → codedna-0.9.2}/dashboard/components/UpgradeBanner.tsx +0 -0
  81. {codedna-0.9.1 → codedna-0.9.2}/dashboard/lib/api.ts +0 -0
  82. {codedna-0.9.1 → codedna-0.9.2}/dashboard/lib/auth.ts +0 -0
  83. {codedna-0.9.1 → codedna-0.9.2}/dashboard/lib/i18n/en.ts +0 -0
  84. {codedna-0.9.1 → codedna-0.9.2}/dashboard/lib/i18n/index.tsx +0 -0
  85. {codedna-0.9.1 → codedna-0.9.2}/dashboard/lib/i18n/tr.ts +0 -0
  86. {codedna-0.9.1 → codedna-0.9.2}/dashboard/lib/plan.ts +0 -0
  87. {codedna-0.9.1 → codedna-0.9.2}/dashboard/next-env.d.ts +0 -0
  88. {codedna-0.9.1 → codedna-0.9.2}/dashboard/next.config.mjs +0 -0
  89. {codedna-0.9.1 → codedna-0.9.2}/dashboard/package-lock.json +0 -0
  90. {codedna-0.9.1 → codedna-0.9.2}/dashboard/package.json +0 -0
  91. {codedna-0.9.1 → codedna-0.9.2}/dashboard/postcss.config.mjs +0 -0
  92. {codedna-0.9.1 → codedna-0.9.2}/dashboard/tailwind.config.ts +0 -0
  93. {codedna-0.9.1 → codedna-0.9.2}/dashboard/tsconfig.json +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codedna
3
- Version: 0.9.1
3
+ Version: 0.9.2
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
@@ -44,10 +44,13 @@ Requires-Dist: uvicorn[standard]>=0.49.0
44
44
  Provides-Extra: dashboard
45
45
  Requires-Dist: next-cmd>=0.5.0; extra == 'dashboard'
46
46
  Provides-Extra: dev
47
- Requires-Dist: black>=24.0.0; extra == 'dev'
48
- Requires-Dist: pytest-cov>=4.0.0; extra == 'dev'
49
- Requires-Dist: pytest>=8.0.0; extra == 'dev'
50
- Requires-Dist: ruff>=0.1.0; extra == 'dev'
47
+ Requires-Dist: black>=26.5.1; extra == 'dev'
48
+ Requires-Dist: pytest-cov>=7.1.0; extra == 'dev'
49
+ Requires-Dist: pytest>=9.1.1; extra == 'dev'
50
+ Requires-Dist: ruff>=0.15.19; extra == 'dev'
51
+ Provides-Extra: nl
52
+ Requires-Dist: huggingface-hub; extra == 'nl'
53
+ Requires-Dist: pyarrow<18,>=14; extra == 'nl'
51
54
  Description-Content-Type: text/markdown
52
55
 
53
56
  <div align="center">
@@ -51,11 +51,9 @@ try:
51
51
  import tree_sitter_typescript as tstypescript
52
52
  _TS_LANG = tstypescript.language_typescript()
53
53
  _TSX_LANG = tstypescript.language_tsx()
54
- _TS_AVAILABLE = True
55
54
  except Exception:
56
55
  _TS_LANG = tsjavascript.language()
57
56
  _TSX_LANG = tsjavascript.language()
58
- _TS_AVAILABLE = False
59
57
 
60
58
  # Supported language map
61
59
  LANGUAGE_MAP: dict[str, tuple] = {
@@ -69,7 +69,7 @@ app = FastAPI(
69
69
  # CORS — fully open for dashboard or external tools
70
70
  app.add_middleware(
71
71
  CORSMiddleware,
72
- allow_origins=["*"],
72
+ allow_origins=["http://localhost:3000"],
73
73
  allow_methods=["*"],
74
74
  allow_headers=["*"],
75
75
  )
@@ -2,6 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import os
5
6
  import sys
6
7
  from datetime import datetime
7
8
  from pathlib import Path
@@ -499,7 +500,6 @@ def serve(
499
500
  repo: Optional[Path] = typer.Option(None, "--repo", "-r", help="Git repo directory"),
500
501
  ) -> None:
501
502
  """Start the FastAPI REST server."""
502
- import os
503
503
  import uvicorn
504
504
 
505
505
  root = repo or find_git_root() or Path.cwd()
@@ -813,7 +813,7 @@ def pr_comment(
813
813
  """Post CodeDNA analysis comment to GitHub PR (updates existing comment, no spam)."""
814
814
  import os
815
815
  from codedna.integrations.github_bot import (
816
- format_pr_comment, post_or_update_comment, github_actions_pr_infosi,
816
+ format_pr_comment, post_or_update_comment, github_actions_pr_bilgisi,
817
817
  )
818
818
  from codedna.tech_debt import calculate_repo_debt
819
819
 
@@ -830,7 +830,7 @@ def pr_comment(
830
830
  target_pr = pr
831
831
 
832
832
  if not target_repo or not target_pr:
833
- info = github_actions_pr_infosi()
833
+ info = github_actions_pr_bilgisi()
834
834
  if info:
835
835
  auto_repo, auto_pr = info
836
836
  target_repo = target_repo or auto_repo
@@ -1130,13 +1130,13 @@ def interview_list(
1130
1130
  table.add_column("Notes", min_width=20)
1131
1131
 
1132
1132
  for o in sessions:
1133
- score_str = f"{o['skor']:.1f}/5" if o["skor"] is not None else "[dim]—[/dim]"
1133
+ score_str = f"{o['score']:.1f}/5" if o["score"] is not None else "[dim]—[/dim]"
1134
1134
  table.add_row(
1135
1135
  str(o["id"]),
1136
- o["candidate"] or o.get("aday") or "?",
1137
- o["start_time"] or o.get("start_date") or "?",
1136
+ o["candidate"] or "?",
1137
+ o["start_time"] or "?",
1138
1138
  score_str,
1139
- (o["notes"] or o.get("notlar") or "")[:30],
1139
+ (o["notes"] or "")[:30],
1140
1140
  )
1141
1141
 
1142
1142
  console.print()
@@ -1188,7 +1188,7 @@ def bus_factor(
1188
1188
  from codedna.bus_factor import calculate_bus_factor, get_at_risk_files, _LARGE_REPO_THRESHOLD
1189
1189
  from codedna.plan import is_feature_available
1190
1190
 
1191
- # Plan check
1191
+ # Plan check
1192
1192
  if not is_feature_available("bus_factor"):
1193
1193
  console.print(
1194
1194
  Panel(
@@ -2041,6 +2041,21 @@ def doctor(
2041
2041
  console.print()
2042
2042
 
2043
2043
 
2044
+ # ---------------------------------------------------------------------------
2045
+ # codedna ask (natural language → command, powered by Needle)
2046
+ # ---------------------------------------------------------------------------
2047
+
2048
+
2049
+ @app.command()
2050
+ def ask(
2051
+ query: list[str] = typer.Argument(..., help="What you want to do, in plain language."),
2052
+ dry_run: bool = typer.Option(False, "--dry-run", help="Show the command without running it."),
2053
+ ) -> None:
2054
+ """Run a CodeDNA command from a natural-language request (powered by Needle)."""
2055
+ from codedna.nl import route_command
2056
+ route_command(" ".join(query), dry_run=dry_run)
2057
+
2058
+
2044
2059
  # ---------------------------------------------------------------------------
2045
2060
  # codedna update (self-upgrade from PyPI)
2046
2061
  # ---------------------------------------------------------------------------
@@ -147,6 +147,7 @@ def save_file_score(
147
147
  complexity_score: float,
148
148
  comment_ratio: float,
149
149
  understanding_score: Optional[float] = None,
150
+ ai_tool_guess: Optional[str] = None,
150
151
  db_path: Optional[Path] = None,
151
152
  ) -> None:
152
153
  """Save a file analysis score."""
@@ -154,10 +155,10 @@ def save_file_score(
154
155
  conn.execute(
155
156
  """
156
157
  INSERT INTO file_scores
157
- (commit_hash, file_path, ai_probability, complexity_score, comment_ratio, understanding_score)
158
- VALUES (?, ?, ?, ?, ?, ?)
158
+ (commit_hash, file_path, ai_probability, complexity_score, comment_ratio, understanding_score, ai_tool_guess)
159
+ VALUES (?, ?, ?, ?, ?, ?, ?)
159
160
  """,
160
- (commit_hash, file_path, ai_probability, complexity_score, comment_ratio, understanding_score),
161
+ (commit_hash, file_path, ai_probability, complexity_score, comment_ratio, understanding_score, ai_tool_guess),
161
162
  )
162
163
 
163
164
 
@@ -227,8 +228,13 @@ def get_all_file_understanding_scores(
227
228
  FROM file_scores fs
228
229
  JOIN commits c ON fs.commit_hash = c.commit_hash
229
230
  WHERE fs.understanding_score IS NOT NULL
230
- GROUP BY fs.file_path
231
- HAVING c.timestamp = MAX(c.timestamp)
231
+ AND c.timestamp = (
232
+ SELECT MAX(c2.timestamp)
233
+ FROM file_scores fs2
234
+ JOIN commits c2 ON fs2.commit_hash = c2.commit_hash
235
+ WHERE fs2.file_path = fs.file_path
236
+ AND fs2.understanding_score IS NOT NULL
237
+ )
232
238
  """,
233
239
  ).fetchall()
234
240
  return {r["file_path"]: float(r["understanding_score"]) for r in rows}
@@ -0,0 +1,234 @@
1
+ """codedna/nl.py — Natural language router for CodeDNA, powered by Needle.
2
+
3
+ Lets a user type a request in plain language and have it translated
4
+ into the matching `codedna` CLI invocation. The model itself is
5
+ multilingual at the input level, so the command works regardless of
6
+ which language the user types in — only the code, comments, and
7
+ output messages are kept in English for consistency with the rest
8
+ of the project.
9
+
10
+ Usage (standalone):
11
+ python -m codedna.nl "show me the AI score for my last commit"
12
+
13
+ Usage (from cli.py):
14
+ from codedna.nl import route_command
15
+ route_command(user_query)
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import json
21
+ import subprocess
22
+ import sys
23
+ from pathlib import Path
24
+
25
+ from needle import SimpleAttentionNetwork, load_checkpoint, generate, get_tokenizer
26
+
27
+ # ---------------------------------------------------------------------------
28
+ # Tool definitions — mirrors codedna_tools.json. Keep these two in sync.
29
+ # ---------------------------------------------------------------------------
30
+
31
+ TOOLS_PATH = Path(__file__).parent / "nl_tools.json"
32
+
33
+
34
+ def _load_tools() -> str:
35
+ if TOOLS_PATH.exists():
36
+ return TOOLS_PATH.read_text(encoding="utf-8")
37
+ raise FileNotFoundError(
38
+ f"{TOOLS_PATH} not found. Copy codedna_tools.json into codedna/ as nl_tools.json."
39
+ )
40
+
41
+
42
+ # ---------------------------------------------------------------------------
43
+ # Needle model (loaded once, lazily)
44
+ # ---------------------------------------------------------------------------
45
+
46
+ # Checkpoint paths. The fine-tuned checkpoint (if present) takes priority
47
+ # over the base Hugging Face download. Both are kept under ~/.codedna/needle/
48
+ # so they are shared across projects and never committed by accident.
49
+ _CHECKPOINT_DIR = Path.home() / ".codedna" / "needle"
50
+ _FINETUNED_PATH = _CHECKPOINT_DIR / "needle-finetuned.pkl"
51
+ _CHECKPOINT_PATH = _CHECKPOINT_DIR / "needle.pkl"
52
+ _HF_REPO_ID = "Cactus-Compute/needle"
53
+ _HF_FILENAME = "needle.pkl"
54
+
55
+ _MODEL = None
56
+ _PARAMS = None
57
+ _TOKENIZER = None
58
+
59
+
60
+ def _resolve_checkpoint() -> Path:
61
+ """Return the best available checkpoint path.
62
+
63
+ Priority:
64
+ 1. Fine-tuned checkpoint at ~/.codedna/needle/needle-finetuned.pkl
65
+ 2. Already-downloaded base model at ~/.codedna/needle/needle.pkl
66
+ 3. Auto-downloaded from Hugging Face, cached at (2)
67
+ """
68
+ if _FINETUNED_PATH.exists():
69
+ return _FINETUNED_PATH
70
+ if _CHECKPOINT_PATH.exists():
71
+ return _CHECKPOINT_PATH
72
+
73
+ try:
74
+ from huggingface_hub import hf_hub_download
75
+ except ImportError as exc:
76
+ raise ImportError(
77
+ "huggingface_hub is required to auto-download the Needle checkpoint. "
78
+ "Install it with: pip install codedna[nl]"
79
+ ) from exc
80
+
81
+ print("First run: downloading the Needle model (~53 MB)...", file=sys.stderr)
82
+ _CHECKPOINT_DIR.mkdir(parents=True, exist_ok=True)
83
+ downloaded_path = hf_hub_download(repo_id=_HF_REPO_ID, filename=_HF_FILENAME)
84
+
85
+ # hf_hub_download caches under ~/.cache/huggingface; copy it into our own
86
+ # checkpoint dir so the path stays stable and predictable for the CLI.
87
+ import shutil
88
+ shutil.copyfile(downloaded_path, _CHECKPOINT_PATH)
89
+ print(f"Needle model cached at {_CHECKPOINT_PATH}", file=sys.stderr)
90
+ return _CHECKPOINT_PATH
91
+
92
+
93
+ def _ensure_model_loaded() -> None:
94
+ global _MODEL, _PARAMS, _TOKENIZER
95
+ if _MODEL is not None:
96
+ return
97
+ checkpoint_path = _resolve_checkpoint()
98
+ params, config = load_checkpoint(str(checkpoint_path))
99
+ _MODEL = SimpleAttentionNetwork(config)
100
+ _PARAMS = params
101
+ _TOKENIZER = get_tokenizer()
102
+
103
+
104
+ # ---------------------------------------------------------------------------
105
+ # Mapping from tool call -> actual `codedna` CLI argv
106
+ # ---------------------------------------------------------------------------
107
+
108
+ def _build_argv(name: str, args: dict) -> list[str]:
109
+ """Translate a Needle tool call into a `codedna ...` argv list."""
110
+
111
+ def opt(flag: str, key: str, cast=str) -> list[str]:
112
+ if key in args and args[key] not in (None, ""):
113
+ return [flag, str(cast(args[key]))]
114
+ return []
115
+
116
+ def flag(name_flag: str, key: str) -> list[str]:
117
+ return [name_flag] if args.get(key) else []
118
+
119
+ mapping = {
120
+ "scan": ["scan", *opt("--max", "max_files", int), *opt("--min-risk", "min_risk", float)],
121
+ "status": ["status"],
122
+ "commit_history": ["history", *opt("--limit", "limit", int)],
123
+ "bus_factor": [
124
+ "bus-factor",
125
+ *flag("--critical", "critical"),
126
+ *opt("--max", "max_files", int),
127
+ ],
128
+ "debt": ["debt", *opt("--rate", "rate", float), *opt("--file", "file")],
129
+ "protect_add": [
130
+ "protect", "add", str(args.get("file_path", "")),
131
+ *opt("--threshold", "threshold", float),
132
+ *opt("--label", "label"),
133
+ ],
134
+ "protect_remove": ["protect", "remove", str(args.get("file_path", ""))],
135
+ "protect_list": ["protect", "list"],
136
+ "protect_check": ["protect", "check"],
137
+ "sprint_create": [
138
+ "sprint", "create",
139
+ "--name", str(args.get("name", "")),
140
+ "--start", str(args.get("start", "")),
141
+ "--end", str(args.get("end", "")),
142
+ ],
143
+ "sprint_health": ["sprint", "health"],
144
+ "sprint_history": ["sprint", "history", *opt("--limit", "limit", int)],
145
+ "export": ["export", *opt("--format", "fmt"), *opt("--output", "output")],
146
+ "webhook_show": ["webhook", "--show"],
147
+ "webhook_test": ["webhook", "--test"],
148
+ "webhook_reset": ["webhook", "--reset"],
149
+ "watch": [
150
+ "watch",
151
+ *opt("--interval", "interval", int),
152
+ *flag("--once", "once"),
153
+ *flag("--notify", "notify"),
154
+ ],
155
+ "report": ["report"],
156
+ "doctor": ["doctor"],
157
+ }
158
+
159
+ if name not in mapping:
160
+ raise ValueError(f"Unknown tool name returned by Needle: {name!r}")
161
+ return mapping[name]
162
+
163
+
164
+ # Commands that change state. At the current model accuracy, a misrouted
165
+ # request here (e.g. "protect" resolving to protect_remove instead of
166
+ # protect_add) can do the opposite of what the user asked. These always
167
+ # require an explicit confirmation, regardless of --dry-run.
168
+ _MUTATING_TOOLS = {
169
+ "protect_add",
170
+ "protect_remove",
171
+ "sprint_create",
172
+ "webhook_reset",
173
+ }
174
+
175
+
176
+ # ---------------------------------------------------------------------------
177
+ # Public entrypoint
178
+ # ---------------------------------------------------------------------------
179
+
180
+ def route_command(user_query: str, dry_run: bool = False) -> None:
181
+ """Parse `user_query` with Needle and run the matching codedna command(s)."""
182
+ _ensure_model_loaded()
183
+
184
+ raw = generate(
185
+ _MODEL, _PARAMS, _TOKENIZER,
186
+ query=user_query,
187
+ tools=_load_tools(),
188
+ stream=False,
189
+ )
190
+
191
+ try:
192
+ calls = json.loads(raw)
193
+ except json.JSONDecodeError:
194
+ print(f"Unexpected output from Needle: {raw}", file=sys.stderr)
195
+ return
196
+
197
+ if not calls:
198
+ print("Could not match that request to a known command. Try rephrasing.")
199
+ return
200
+
201
+ for call in calls:
202
+ name = call.get("name")
203
+ args = call.get("arguments", {})
204
+ try:
205
+ argv = _build_argv(name, args)
206
+ except ValueError as exc:
207
+ print(str(exc), file=sys.stderr)
208
+ continue
209
+
210
+ full_cmd = ["codedna", *argv]
211
+ print(f"-> {' '.join(full_cmd)}")
212
+
213
+ if dry_run:
214
+ continue
215
+
216
+ if name in _MUTATING_TOOLS:
217
+ # Needle is a small (26M parameter) model and can misroute
218
+ # requests, especially the difference between "protect" and
219
+ # "unprotect" intents. Never run a state-changing command
220
+ # silently — always get an explicit yes from the user first.
221
+ answer = input("Run this command? [y/N] ").strip().lower()
222
+ if answer not in ("y", "yes"):
223
+ print("Skipped.")
224
+ continue
225
+
226
+ subprocess.run(full_cmd)
227
+
228
+
229
+ if __name__ == "__main__":
230
+ query = " ".join(sys.argv[1:])
231
+ if not query:
232
+ print("Usage: python -m codedna.nl '<your request in natural language>'")
233
+ sys.exit(1)
234
+ route_command(query)
@@ -0,0 +1,127 @@
1
+ [
2
+ {
3
+ "name": "scan",
4
+ "description": "Scan the current repo and show an AI-generated code risk report.",
5
+ "parameters": {
6
+ "max_files": {"type": "integer", "description": "Maximum number of files to scan. Defaults to 200.", "required": false},
7
+ "min_risk": {"type": "number", "description": "Minimum AI probability filter, 0.0 to 1.0.", "required": false}
8
+ }
9
+ },
10
+ {
11
+ "name": "status",
12
+ "description": "Show the AI score and commit message quality for the last commit.",
13
+ "parameters": {}
14
+ },
15
+ {
16
+ "name": "commit_history",
17
+ "description": "Show a table of past commits with their AI scores and understanding scores. This is about commit history, not sprint history.",
18
+ "parameters": {
19
+ "limit": {"type": "integer", "description": "Number of past commits to show. Defaults to 20.", "required": false}
20
+ }
21
+ },
22
+ {
23
+ "name": "bus_factor",
24
+ "description": "Run repo-wide bus factor analysis and show ownership risks.",
25
+ "parameters": {
26
+ "critical": {"type": "boolean", "description": "Show only critical files where bus factor equals 1.", "required": false},
27
+ "max_files": {"type": "integer", "description": "Maximum number of files to process. Defaults to 500.", "required": false}
28
+ }
29
+ },
30
+ {
31
+ "name": "debt",
32
+ "description": "Calculate technical debt cost in dollars for the whole repository. Only set the file parameter if the user names a specific file path explicitly.",
33
+ "parameters": {
34
+ "rate": {"type": "number", "description": "Hourly developer rate in dollars. Defaults to 75.", "required": false},
35
+ "file": {"type": "string", "description": "An actual file path, e.g. 'src/auth.py'. Do not fill this with a description or topic — only a real path the user typed.", "required": false}
36
+ }
37
+ },
38
+ {
39
+ "name": "protect_add",
40
+ "description": "Add protection to a file or directory so AI is not allowed to edit it. Use this when the user wants to START protecting, guard, or lock a path. This is the opposite of protect_remove.",
41
+ "parameters": {
42
+ "file_path": {"type": "string", "description": "Path to protect, e.g. 'auth/' or 'payment/login.py'.", "required": true},
43
+ "threshold": {"type": "number", "description": "Minimum required understanding score, 0 to 5. Defaults to 3.5.", "required": false},
44
+ "label": {"type": "string", "description": "Human-readable label for this protected module.", "required": false}
45
+ }
46
+ },
47
+ {
48
+ "name": "protect_remove",
49
+ "description": "Remove protection from a previously protected file, so AI is allowed to edit it again. Use this when the user wants to STOP, unprotect, lift protection, or remove the protected status from a path. This is the opposite of protect_add.",
50
+ "parameters": {
51
+ "file_path": {"type": "string", "description": "Path to unprotect.", "required": true}
52
+ }
53
+ },
54
+ {
55
+ "name": "protect_list",
56
+ "description": "Show all protected modules and their current status.",
57
+ "parameters": {}
58
+ },
59
+ {
60
+ "name": "protect_check",
61
+ "description": "Show only protected modules that currently violate their understanding threshold.",
62
+ "parameters": {}
63
+ },
64
+ {
65
+ "name": "sprint_create",
66
+ "description": "Create a new sprint and calculate its health score.",
67
+ "parameters": {
68
+ "name": {"type": "string", "description": "Sprint name, e.g. 'Sprint 14'.", "required": true},
69
+ "start": {"type": "string", "description": "Start date in YYYY-MM-DD format.", "required": true},
70
+ "end": {"type": "string", "description": "End date in YYYY-MM-DD format.", "required": true}
71
+ }
72
+ },
73
+ {
74
+ "name": "sprint_health",
75
+ "description": "Show the latest sprint's health score, velocity, and quality.",
76
+ "parameters": {}
77
+ },
78
+ {
79
+ "name": "sprint_history",
80
+ "description": "Show past sprints as a table.",
81
+ "parameters": {
82
+ "limit": {"type": "integer", "description": "Number of past sprints to show. Defaults to 10.", "required": false}
83
+ }
84
+ },
85
+ {
86
+ "name": "export",
87
+ "description": "Export all CodeDNA data as JSON or CSV.",
88
+ "parameters": {
89
+ "fmt": {"type": "string", "description": "Export format: 'json' or 'csv'. Defaults to json.", "required": false},
90
+ "output": {"type": "string", "description": "Output file path. If omitted, prints to stdout.", "required": false}
91
+ }
92
+ },
93
+ {
94
+ "name": "webhook_show",
95
+ "description": "Show the current Slack or Discord webhook configuration.",
96
+ "parameters": {}
97
+ },
98
+ {
99
+ "name": "webhook_test",
100
+ "description": "Send a test webhook notification to Slack or Discord.",
101
+ "parameters": {}
102
+ },
103
+ {
104
+ "name": "webhook_reset",
105
+ "description": "Clear the current webhook configuration.",
106
+ "parameters": {}
107
+ },
108
+ {
109
+ "name": "watch",
110
+ "description": "Watch the git repo for new commits and analyze them automatically.",
111
+ "parameters": {
112
+ "interval": {"type": "integer", "description": "Poll interval in seconds. Defaults to 30.", "required": false},
113
+ "once": {"type": "boolean", "description": "Run once and exit, useful for cron jobs.", "required": false},
114
+ "notify": {"type": "boolean", "description": "Send a webhook notification when a new commit is found.", "required": false}
115
+ }
116
+ },
117
+ {
118
+ "name": "report",
119
+ "description": "Generate an HTML report for the repo.",
120
+ "parameters": {}
121
+ },
122
+ {
123
+ "name": "doctor",
124
+ "description": "Run a system health check on the CodeDNA installation itself (dependencies, config, database). This is about the CodeDNA tool's own health, not sprint health or project health.",
125
+ "parameters": {}
126
+ }
127
+ ]
@@ -74,7 +74,8 @@ def calculate_single_commit_ratio(repo: Repo, file_path: str) -> float:
74
74
  largest = max(commit_contributions)
75
75
  return min(largest / total, 1.0)
76
76
 
77
- except Exception:
77
+ except Exception as exc:
78
+ console.log(f"[dim]single_commit_ratio error for {file_path}: {exc}[/dim]")
78
79
  return 0.0
79
80
 
80
81
 
@@ -128,7 +128,7 @@ def calculate_file_debt(
128
128
  try:
129
129
  from codedna.analyzer import analyze_file
130
130
  result = analyze_file(p)
131
- if result.desteklenmiyor or result.hata:
131
+ if result.unsupported or result.error:
132
132
  return None
133
133
  ai = result.ai_probability
134
134
  complexity = result.complexity_score
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "codedna"
3
- version = "0.9.1"
3
+ version = "0.9.2"
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"
@@ -50,11 +50,16 @@ dependencies = [
50
50
  dashboard = [
51
51
  "next-cmd>=0.5.0", # Next.js process management
52
52
  ]
53
+ nl = [
54
+ # "needle @ git+https://github.com/cactus-compute/needle.git", # TODO: publish needle to PyPI first
55
+ "huggingface_hub",
56
+ "pyarrow>=14,<18",
57
+ ]
53
58
  dev = [
54
- "pytest>=8.0.0",
55
- "pytest-cov>=4.0.0",
56
- "black>=24.0.0",
57
- "ruff>=0.1.0",
59
+ "pytest>=9.1.1",
60
+ "pytest-cov>=7.1.0",
61
+ "black>=26.5.1",
62
+ "ruff>=0.15.19",
58
63
  ]
59
64
 
60
65
  [project.urls]
@@ -66,6 +71,9 @@ Changelog = "https://github.com/natureco-official/codedna/blob/main/CHANGELOG.md
66
71
  [project.scripts]
67
72
  codedna = "codedna.cli:main"
68
73
 
74
+ [tool.hatch.metadata]
75
+ allow-direct-references = true
76
+
69
77
  [build-system]
70
78
  requires = ["hatchling"]
71
79
  build-backend = "hatchling.build"
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