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.
- {codedna-0.9.1 → codedna-0.9.2}/PKG-INFO +8 -5
- {codedna-0.9.1 → codedna-0.9.2}/codedna/analyzer.py +0 -2
- {codedna-0.9.1 → codedna-0.9.2}/codedna/api.py +1 -1
- {codedna-0.9.1 → codedna-0.9.2}/codedna/cli.py +23 -8
- {codedna-0.9.1 → codedna-0.9.2}/codedna/db.py +11 -5
- codedna-0.9.2/codedna/nl.py +234 -0
- codedna-0.9.2/codedna/nl_tools.json +127 -0
- {codedna-0.9.1 → codedna-0.9.2}/codedna/scorer.py +2 -1
- {codedna-0.9.1 → codedna-0.9.2}/codedna/tech_debt.py +1 -1
- {codedna-0.9.1 → codedna-0.9.2}/pyproject.toml +13 -5
- {codedna-0.9.1 → codedna-0.9.2}/.gitignore +0 -0
- {codedna-0.9.1 → codedna-0.9.2}/LICENSE +0 -0
- {codedna-0.9.1 → codedna-0.9.2}/README.md +0 -0
- {codedna-0.9.1 → codedna-0.9.2}/codedna/__init__.py +0 -0
- {codedna-0.9.1 → codedna-0.9.2}/codedna/ai.py +0 -0
- {codedna-0.9.1 → codedna-0.9.2}/codedna/ai_box.py +0 -0
- {codedna-0.9.1 → codedna-0.9.2}/codedna/ai_fingerprint.py +0 -0
- {codedna-0.9.1 → codedna-0.9.2}/codedna/auth.py +0 -0
- {codedna-0.9.1 → codedna-0.9.2}/codedna/bus_factor.py +0 -0
- {codedna-0.9.1 → codedna-0.9.2}/codedna/demo.py +0 -0
- {codedna-0.9.1 → codedna-0.9.2}/codedna/git_hook.py +0 -0
- {codedna-0.9.1 → codedna-0.9.2}/codedna/integrations/__init__.py +0 -0
- {codedna-0.9.1 → codedna-0.9.2}/codedna/integrations/github_bot.py +0 -0
- {codedna-0.9.1 → codedna-0.9.2}/codedna/integrations/jira.py +0 -0
- {codedna-0.9.1 → codedna-0.9.2}/codedna/integrations/lemonsqueezy.py +0 -0
- {codedna-0.9.1 → codedna-0.9.2}/codedna/interview.py +0 -0
- {codedna-0.9.1 → codedna-0.9.2}/codedna/onboarding.py +0 -0
- {codedna-0.9.1 → codedna-0.9.2}/codedna/plan.py +0 -0
- {codedna-0.9.1 → codedna-0.9.2}/codedna/protection.py +0 -0
- {codedna-0.9.1 → codedna-0.9.2}/codedna/rate_limit.py +0 -0
- {codedna-0.9.1 → codedna-0.9.2}/codedna/sprint_health.py +0 -0
- {codedna-0.9.1 → codedna-0.9.2}/codedna/survey.py +0 -0
- {codedna-0.9.1 → codedna-0.9.2}/codedna/webhooks.py +0 -0
- {codedna-0.9.1 → codedna-0.9.2}/dashboard/.env.local.example +0 -0
- {codedna-0.9.1 → codedna-0.9.2}/dashboard/.gitignore +0 -0
- {codedna-0.9.1 → codedna-0.9.2}/dashboard/Dockerfile +0 -0
- {codedna-0.9.1 → codedna-0.9.2}/dashboard/README.md +0 -0
- {codedna-0.9.1 → codedna-0.9.2}/dashboard/app/ai-compare/page.tsx +0 -0
- {codedna-0.9.1 → codedna-0.9.2}/dashboard/app/api/auth/login/route.ts +0 -0
- {codedna-0.9.1 → codedna-0.9.2}/dashboard/app/api/auth/logout/route.ts +0 -0
- {codedna-0.9.1 → codedna-0.9.2}/dashboard/app/api/auth/me/route.ts +0 -0
- {codedna-0.9.1 → codedna-0.9.2}/dashboard/app/api/auth/register/route.ts +0 -0
- {codedna-0.9.1 → codedna-0.9.2}/dashboard/app/api/billing/checkout/route.ts +0 -0
- {codedna-0.9.1 → codedna-0.9.2}/dashboard/app/billing/page.tsx +0 -0
- {codedna-0.9.1 → codedna-0.9.2}/dashboard/app/bus-factor/page.tsx +0 -0
- {codedna-0.9.1 → codedna-0.9.2}/dashboard/app/commits/page.tsx +0 -0
- {codedna-0.9.1 → codedna-0.9.2}/dashboard/app/debt/page.tsx +0 -0
- {codedna-0.9.1 → codedna-0.9.2}/dashboard/app/favicon.ico +0 -0
- {codedna-0.9.1 → codedna-0.9.2}/dashboard/app/feedback/page.tsx +0 -0
- {codedna-0.9.1 → codedna-0.9.2}/dashboard/app/files/page.tsx +0 -0
- {codedna-0.9.1 → codedna-0.9.2}/dashboard/app/fonts/GeistMonoVF.woff +0 -0
- {codedna-0.9.1 → codedna-0.9.2}/dashboard/app/fonts/GeistVF.woff +0 -0
- {codedna-0.9.1 → codedna-0.9.2}/dashboard/app/globals.css +0 -0
- {codedna-0.9.1 → codedna-0.9.2}/dashboard/app/interview/page.tsx +0 -0
- {codedna-0.9.1 → codedna-0.9.2}/dashboard/app/layout.tsx +0 -0
- {codedna-0.9.1 → codedna-0.9.2}/dashboard/app/login/page.tsx +0 -0
- {codedna-0.9.1 → codedna-0.9.2}/dashboard/app/onboarding/page.tsx +0 -0
- {codedna-0.9.1 → codedna-0.9.2}/dashboard/app/page.tsx +0 -0
- {codedna-0.9.1 → codedna-0.9.2}/dashboard/app/pricing/page.tsx +0 -0
- {codedna-0.9.1 → codedna-0.9.2}/dashboard/app/protected/page.tsx +0 -0
- {codedna-0.9.1 → codedna-0.9.2}/dashboard/app/register/page.tsx +0 -0
- {codedna-0.9.1 → codedna-0.9.2}/dashboard/app/repos/page.tsx +0 -0
- {codedna-0.9.1 → codedna-0.9.2}/dashboard/app/settings/integrations/page.tsx +0 -0
- {codedna-0.9.1 → codedna-0.9.2}/dashboard/app/sprints/page.tsx +0 -0
- {codedna-0.9.1 → codedna-0.9.2}/dashboard/app/trends/page.tsx +0 -0
- {codedna-0.9.1 → codedna-0.9.2}/dashboard/components/AuthProvider.tsx +0 -0
- {codedna-0.9.1 → codedna-0.9.2}/dashboard/components/ClientProviders.tsx +0 -0
- {codedna-0.9.1 → codedna-0.9.2}/dashboard/components/CommitTable.tsx +0 -0
- {codedna-0.9.1 → codedna-0.9.2}/dashboard/components/CostInfoTooltip.tsx +0 -0
- {codedna-0.9.1 → codedna-0.9.2}/dashboard/components/ErrorBanner.tsx +0 -0
- {codedna-0.9.1 → codedna-0.9.2}/dashboard/components/FeatureGate.tsx +0 -0
- {codedna-0.9.1 → codedna-0.9.2}/dashboard/components/FileHeatmap.tsx +0 -0
- {codedna-0.9.1 → codedna-0.9.2}/dashboard/components/LanguageSwitcher.tsx +0 -0
- {codedna-0.9.1 → codedna-0.9.2}/dashboard/components/NavClient.tsx +0 -0
- {codedna-0.9.1 → codedna-0.9.2}/dashboard/components/OverviewTitles.tsx +0 -0
- {codedna-0.9.1 → codedna-0.9.2}/dashboard/components/QuickInsights.tsx +0 -0
- {codedna-0.9.1 → codedna-0.9.2}/dashboard/components/RiskBadge.tsx +0 -0
- {codedna-0.9.1 → codedna-0.9.2}/dashboard/components/SummaryCards.tsx +0 -0
- {codedna-0.9.1 → codedna-0.9.2}/dashboard/components/UnderstandingChart.tsx +0 -0
- {codedna-0.9.1 → codedna-0.9.2}/dashboard/components/UpgradeBanner.tsx +0 -0
- {codedna-0.9.1 → codedna-0.9.2}/dashboard/lib/api.ts +0 -0
- {codedna-0.9.1 → codedna-0.9.2}/dashboard/lib/auth.ts +0 -0
- {codedna-0.9.1 → codedna-0.9.2}/dashboard/lib/i18n/en.ts +0 -0
- {codedna-0.9.1 → codedna-0.9.2}/dashboard/lib/i18n/index.tsx +0 -0
- {codedna-0.9.1 → codedna-0.9.2}/dashboard/lib/i18n/tr.ts +0 -0
- {codedna-0.9.1 → codedna-0.9.2}/dashboard/lib/plan.ts +0 -0
- {codedna-0.9.1 → codedna-0.9.2}/dashboard/next-env.d.ts +0 -0
- {codedna-0.9.1 → codedna-0.9.2}/dashboard/next.config.mjs +0 -0
- {codedna-0.9.1 → codedna-0.9.2}/dashboard/package-lock.json +0 -0
- {codedna-0.9.1 → codedna-0.9.2}/dashboard/package.json +0 -0
- {codedna-0.9.1 → codedna-0.9.2}/dashboard/postcss.config.mjs +0 -0
- {codedna-0.9.1 → codedna-0.9.2}/dashboard/tailwind.config.ts +0 -0
- {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.
|
|
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>=
|
|
48
|
-
Requires-Dist: pytest-cov>=
|
|
49
|
-
Requires-Dist: pytest>=
|
|
50
|
-
Requires-Dist: ruff>=0.
|
|
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] = {
|
|
@@ -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,
|
|
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 =
|
|
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['
|
|
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
|
|
1137
|
-
o["start_time"] or
|
|
1136
|
+
o["candidate"] or "?",
|
|
1137
|
+
o["start_time"] or "?",
|
|
1138
1138
|
score_str,
|
|
1139
|
-
(o["notes"] or
|
|
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
|
-
|
|
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
|
-
|
|
231
|
-
|
|
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.
|
|
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.
|
|
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>=
|
|
55
|
-
"pytest-cov>=
|
|
56
|
-
"black>=
|
|
57
|
-
"ruff>=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
|
|
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
|