codedna 0.9.1__tar.gz → 0.9.3__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.3}/PKG-INFO +8 -5
- {codedna-0.9.1 → codedna-0.9.3}/codedna/__init__.py +1 -1
- {codedna-0.9.1 → codedna-0.9.3}/codedna/analyzer.py +0 -2
- {codedna-0.9.1 → codedna-0.9.3}/codedna/api.py +1 -1
- {codedna-0.9.1 → codedna-0.9.3}/codedna/cli.py +108 -11
- {codedna-0.9.1 → codedna-0.9.3}/codedna/db.py +11 -5
- codedna-0.9.3/codedna/natureco_account.py +200 -0
- codedna-0.9.3/codedna/nl.py +234 -0
- codedna-0.9.3/codedna/nl_tools.json +127 -0
- {codedna-0.9.1 → codedna-0.9.3}/codedna/scorer.py +6 -2
- {codedna-0.9.1 → codedna-0.9.3}/codedna/tech_debt.py +1 -1
- {codedna-0.9.1 → codedna-0.9.3}/pyproject.toml +12 -5
- {codedna-0.9.1 → codedna-0.9.3}/.gitignore +0 -0
- {codedna-0.9.1 → codedna-0.9.3}/LICENSE +0 -0
- {codedna-0.9.1 → codedna-0.9.3}/README.md +0 -0
- {codedna-0.9.1 → codedna-0.9.3}/codedna/ai.py +0 -0
- {codedna-0.9.1 → codedna-0.9.3}/codedna/ai_box.py +0 -0
- {codedna-0.9.1 → codedna-0.9.3}/codedna/ai_fingerprint.py +0 -0
- {codedna-0.9.1 → codedna-0.9.3}/codedna/auth.py +0 -0
- {codedna-0.9.1 → codedna-0.9.3}/codedna/bus_factor.py +0 -0
- {codedna-0.9.1 → codedna-0.9.3}/codedna/demo.py +0 -0
- {codedna-0.9.1 → codedna-0.9.3}/codedna/git_hook.py +0 -0
- {codedna-0.9.1 → codedna-0.9.3}/codedna/integrations/__init__.py +0 -0
- {codedna-0.9.1 → codedna-0.9.3}/codedna/integrations/github_bot.py +0 -0
- {codedna-0.9.1 → codedna-0.9.3}/codedna/integrations/jira.py +0 -0
- {codedna-0.9.1 → codedna-0.9.3}/codedna/integrations/lemonsqueezy.py +0 -0
- {codedna-0.9.1 → codedna-0.9.3}/codedna/interview.py +0 -0
- {codedna-0.9.1 → codedna-0.9.3}/codedna/onboarding.py +0 -0
- {codedna-0.9.1 → codedna-0.9.3}/codedna/plan.py +0 -0
- {codedna-0.9.1 → codedna-0.9.3}/codedna/protection.py +0 -0
- {codedna-0.9.1 → codedna-0.9.3}/codedna/rate_limit.py +0 -0
- {codedna-0.9.1 → codedna-0.9.3}/codedna/sprint_health.py +0 -0
- {codedna-0.9.1 → codedna-0.9.3}/codedna/survey.py +0 -0
- {codedna-0.9.1 → codedna-0.9.3}/codedna/webhooks.py +0 -0
- {codedna-0.9.1 → codedna-0.9.3}/dashboard/.env.local.example +0 -0
- {codedna-0.9.1 → codedna-0.9.3}/dashboard/.gitignore +0 -0
- {codedna-0.9.1 → codedna-0.9.3}/dashboard/Dockerfile +0 -0
- {codedna-0.9.1 → codedna-0.9.3}/dashboard/README.md +0 -0
- {codedna-0.9.1 → codedna-0.9.3}/dashboard/app/ai-compare/page.tsx +0 -0
- {codedna-0.9.1 → codedna-0.9.3}/dashboard/app/api/auth/login/route.ts +0 -0
- {codedna-0.9.1 → codedna-0.9.3}/dashboard/app/api/auth/logout/route.ts +0 -0
- {codedna-0.9.1 → codedna-0.9.3}/dashboard/app/api/auth/me/route.ts +0 -0
- {codedna-0.9.1 → codedna-0.9.3}/dashboard/app/api/auth/register/route.ts +0 -0
- {codedna-0.9.1 → codedna-0.9.3}/dashboard/app/api/billing/checkout/route.ts +0 -0
- {codedna-0.9.1 → codedna-0.9.3}/dashboard/app/billing/page.tsx +0 -0
- {codedna-0.9.1 → codedna-0.9.3}/dashboard/app/bus-factor/page.tsx +0 -0
- {codedna-0.9.1 → codedna-0.9.3}/dashboard/app/commits/page.tsx +0 -0
- {codedna-0.9.1 → codedna-0.9.3}/dashboard/app/debt/page.tsx +0 -0
- {codedna-0.9.1 → codedna-0.9.3}/dashboard/app/favicon.ico +0 -0
- {codedna-0.9.1 → codedna-0.9.3}/dashboard/app/feedback/page.tsx +0 -0
- {codedna-0.9.1 → codedna-0.9.3}/dashboard/app/files/page.tsx +0 -0
- {codedna-0.9.1 → codedna-0.9.3}/dashboard/app/fonts/GeistMonoVF.woff +0 -0
- {codedna-0.9.1 → codedna-0.9.3}/dashboard/app/fonts/GeistVF.woff +0 -0
- {codedna-0.9.1 → codedna-0.9.3}/dashboard/app/globals.css +0 -0
- {codedna-0.9.1 → codedna-0.9.3}/dashboard/app/interview/page.tsx +0 -0
- {codedna-0.9.1 → codedna-0.9.3}/dashboard/app/layout.tsx +0 -0
- {codedna-0.9.1 → codedna-0.9.3}/dashboard/app/login/page.tsx +0 -0
- {codedna-0.9.1 → codedna-0.9.3}/dashboard/app/onboarding/page.tsx +0 -0
- {codedna-0.9.1 → codedna-0.9.3}/dashboard/app/page.tsx +0 -0
- {codedna-0.9.1 → codedna-0.9.3}/dashboard/app/pricing/page.tsx +0 -0
- {codedna-0.9.1 → codedna-0.9.3}/dashboard/app/protected/page.tsx +0 -0
- {codedna-0.9.1 → codedna-0.9.3}/dashboard/app/register/page.tsx +0 -0
- {codedna-0.9.1 → codedna-0.9.3}/dashboard/app/repos/page.tsx +0 -0
- {codedna-0.9.1 → codedna-0.9.3}/dashboard/app/settings/integrations/page.tsx +0 -0
- {codedna-0.9.1 → codedna-0.9.3}/dashboard/app/sprints/page.tsx +0 -0
- {codedna-0.9.1 → codedna-0.9.3}/dashboard/app/trends/page.tsx +0 -0
- {codedna-0.9.1 → codedna-0.9.3}/dashboard/components/AuthProvider.tsx +0 -0
- {codedna-0.9.1 → codedna-0.9.3}/dashboard/components/ClientProviders.tsx +0 -0
- {codedna-0.9.1 → codedna-0.9.3}/dashboard/components/CommitTable.tsx +0 -0
- {codedna-0.9.1 → codedna-0.9.3}/dashboard/components/CostInfoTooltip.tsx +0 -0
- {codedna-0.9.1 → codedna-0.9.3}/dashboard/components/ErrorBanner.tsx +0 -0
- {codedna-0.9.1 → codedna-0.9.3}/dashboard/components/FeatureGate.tsx +0 -0
- {codedna-0.9.1 → codedna-0.9.3}/dashboard/components/FileHeatmap.tsx +0 -0
- {codedna-0.9.1 → codedna-0.9.3}/dashboard/components/LanguageSwitcher.tsx +0 -0
- {codedna-0.9.1 → codedna-0.9.3}/dashboard/components/NavClient.tsx +0 -0
- {codedna-0.9.1 → codedna-0.9.3}/dashboard/components/OverviewTitles.tsx +0 -0
- {codedna-0.9.1 → codedna-0.9.3}/dashboard/components/QuickInsights.tsx +0 -0
- {codedna-0.9.1 → codedna-0.9.3}/dashboard/components/RiskBadge.tsx +0 -0
- {codedna-0.9.1 → codedna-0.9.3}/dashboard/components/SummaryCards.tsx +0 -0
- {codedna-0.9.1 → codedna-0.9.3}/dashboard/components/UnderstandingChart.tsx +0 -0
- {codedna-0.9.1 → codedna-0.9.3}/dashboard/components/UpgradeBanner.tsx +0 -0
- {codedna-0.9.1 → codedna-0.9.3}/dashboard/lib/api.ts +0 -0
- {codedna-0.9.1 → codedna-0.9.3}/dashboard/lib/auth.ts +0 -0
- {codedna-0.9.1 → codedna-0.9.3}/dashboard/lib/i18n/en.ts +0 -0
- {codedna-0.9.1 → codedna-0.9.3}/dashboard/lib/i18n/index.tsx +0 -0
- {codedna-0.9.1 → codedna-0.9.3}/dashboard/lib/i18n/tr.ts +0 -0
- {codedna-0.9.1 → codedna-0.9.3}/dashboard/lib/plan.ts +0 -0
- {codedna-0.9.1 → codedna-0.9.3}/dashboard/next-env.d.ts +0 -0
- {codedna-0.9.1 → codedna-0.9.3}/dashboard/next.config.mjs +0 -0
- {codedna-0.9.1 → codedna-0.9.3}/dashboard/package-lock.json +0 -0
- {codedna-0.9.1 → codedna-0.9.3}/dashboard/package.json +0 -0
- {codedna-0.9.1 → codedna-0.9.3}/dashboard/postcss.config.mjs +0 -0
- {codedna-0.9.1 → codedna-0.9.3}/dashboard/tailwind.config.ts +0 -0
- {codedna-0.9.1 → codedna-0.9.3}/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.3
|
|
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
|
|
@@ -131,6 +132,52 @@ def init(
|
|
|
131
132
|
console.print(Panel(info, border_style="green", padding=(1, 2)))
|
|
132
133
|
|
|
133
134
|
|
|
135
|
+
# ---------------------------------------------------------------------------
|
|
136
|
+
# codedna account (NatureCo SSO — shared with the NatureCo CLI & terminal)
|
|
137
|
+
# ---------------------------------------------------------------------------
|
|
138
|
+
@app.command()
|
|
139
|
+
def account(
|
|
140
|
+
action: str = typer.Argument("whoami", help="login | logout | whoami"),
|
|
141
|
+
) -> None:
|
|
142
|
+
"""Sign in with your NatureCo account (single account across the ecosystem)."""
|
|
143
|
+
from codedna import natureco_account as nc
|
|
144
|
+
|
|
145
|
+
if action == "whoami":
|
|
146
|
+
me = nc.whoami()
|
|
147
|
+
if me:
|
|
148
|
+
console.print(f"[green]✓[/green] Signed in as [bold]{me.get('email')}[/bold]")
|
|
149
|
+
else:
|
|
150
|
+
console.print("[yellow]Not signed in.[/yellow] Run [bold cyan]codedna account login[/bold cyan]")
|
|
151
|
+
return
|
|
152
|
+
|
|
153
|
+
if action == "logout":
|
|
154
|
+
nc.logout()
|
|
155
|
+
console.print("[green]✓[/green] Signed out.")
|
|
156
|
+
return
|
|
157
|
+
|
|
158
|
+
if action == "login":
|
|
159
|
+
email = typer.prompt("NatureCo email").strip()
|
|
160
|
+
use_pw = typer.confirm("Sign in with a password? (No = email a code/login link)", default=False)
|
|
161
|
+
try:
|
|
162
|
+
if use_pw:
|
|
163
|
+
password = typer.prompt("Password", hide_input=True)
|
|
164
|
+
res = nc.login_with_password(email, password)
|
|
165
|
+
else:
|
|
166
|
+
nc.send_otp(email)
|
|
167
|
+
console.print("[dim]Check your email — paste the 6-digit code or the full login link.[/dim]")
|
|
168
|
+
value = typer.prompt("Code or login link").strip()
|
|
169
|
+
res = nc.verify_link(value) if "://" in value else nc.verify_otp(email, value)
|
|
170
|
+
who = (res.get("user") or {}).get("email") or email
|
|
171
|
+
console.print(f"[green]✓ Signed in as[/green] [bold]{who}[/bold]")
|
|
172
|
+
except Exception as e: # noqa: BLE001
|
|
173
|
+
console.print(f"[red]✗ Sign-in failed:[/red] {e}")
|
|
174
|
+
raise typer.Exit(1) from e
|
|
175
|
+
return
|
|
176
|
+
|
|
177
|
+
console.print(f"[red]Unknown action:[/red] {action} [dim](use: login | logout | whoami)[/dim]")
|
|
178
|
+
raise typer.Exit(1)
|
|
179
|
+
|
|
180
|
+
|
|
134
181
|
# ---------------------------------------------------------------------------
|
|
135
182
|
# codedna scan
|
|
136
183
|
# ---------------------------------------------------------------------------
|
|
@@ -139,17 +186,29 @@ def scan(
|
|
|
139
186
|
repo: Optional[Path] = typer.Option(None, "--repo", "-r", help="Git repo directory"),
|
|
140
187
|
max_files: int = typer.Option(200, "--max", "-m", help="Maximum number of files to scan"),
|
|
141
188
|
min_risk: float = typer.Option(0.0, "--min-risk", help="Minimum AI probability filter (0.0-1.0)"),
|
|
189
|
+
json_out: bool = typer.Option(False, "--json", help="Output machine-readable JSON (for tooling)"),
|
|
142
190
|
) -> None:
|
|
143
191
|
"""Scan current repo and show AI risk report."""
|
|
144
|
-
|
|
145
|
-
|
|
192
|
+
if not json_out:
|
|
193
|
+
console.print()
|
|
194
|
+
console.print("[bold cyan]🧬 CodeDNA[/bold cyan] — Scanning repo...\n")
|
|
146
195
|
|
|
147
196
|
root = repo or find_git_root() or Path.cwd()
|
|
148
197
|
|
|
149
|
-
|
|
198
|
+
if json_out:
|
|
150
199
|
results = scan_repository(root, max_files=max_files)
|
|
200
|
+
else:
|
|
201
|
+
with console.status("[dim]Analyzing files...[/dim]"):
|
|
202
|
+
results = scan_repository(root, max_files=max_files)
|
|
151
203
|
|
|
152
204
|
if not results:
|
|
205
|
+
if json_out:
|
|
206
|
+
import json as _json
|
|
207
|
+
print(_json.dumps({
|
|
208
|
+
"version": __version__, "repo": str(root), "file_count": 0,
|
|
209
|
+
"avg_ai_probability": 0.0, "max_ai_probability": 0.0, "files": [],
|
|
210
|
+
}, indent=2))
|
|
211
|
+
raise typer.Exit(0)
|
|
153
212
|
console.print("[yellow]No supported files found to scan.[/yellow]")
|
|
154
213
|
console.print("[dim]Supported: .py .js .jsx .ts .tsx[/dim]")
|
|
155
214
|
return
|
|
@@ -169,6 +228,30 @@ def scan(
|
|
|
169
228
|
except Exception:
|
|
170
229
|
pass
|
|
171
230
|
|
|
231
|
+
# --json: machine-readable output for tooling (e.g. the NatureCo CLI `dna` command)
|
|
232
|
+
if json_out:
|
|
233
|
+
import json as _json
|
|
234
|
+
avg_ai = (sum(s.ai_probability for s in results) / len(results)) if results else 0.0
|
|
235
|
+
payload = {
|
|
236
|
+
"version": __version__,
|
|
237
|
+
"repo": str(root),
|
|
238
|
+
"file_count": len(results),
|
|
239
|
+
"avg_ai_probability": round(avg_ai, 4),
|
|
240
|
+
"max_ai_probability": round(max((s.ai_probability for s in results), default=0.0), 4),
|
|
241
|
+
"files": [
|
|
242
|
+
{
|
|
243
|
+
"file": s.file_path,
|
|
244
|
+
"ai_probability": round(s.ai_probability, 4),
|
|
245
|
+
"complexity": s.complexity_label,
|
|
246
|
+
"lines": s.total_lines,
|
|
247
|
+
"understanding": understanding_scores_map.get(s.file_path),
|
|
248
|
+
}
|
|
249
|
+
for s in results
|
|
250
|
+
],
|
|
251
|
+
}
|
|
252
|
+
print(_json.dumps(payload, indent=2, default=str))
|
|
253
|
+
raise typer.Exit(0)
|
|
254
|
+
|
|
172
255
|
# Build table
|
|
173
256
|
table = Table(
|
|
174
257
|
title="",
|
|
@@ -499,7 +582,6 @@ def serve(
|
|
|
499
582
|
repo: Optional[Path] = typer.Option(None, "--repo", "-r", help="Git repo directory"),
|
|
500
583
|
) -> None:
|
|
501
584
|
"""Start the FastAPI REST server."""
|
|
502
|
-
import os
|
|
503
585
|
import uvicorn
|
|
504
586
|
|
|
505
587
|
root = repo or find_git_root() or Path.cwd()
|
|
@@ -813,7 +895,7 @@ def pr_comment(
|
|
|
813
895
|
"""Post CodeDNA analysis comment to GitHub PR (updates existing comment, no spam)."""
|
|
814
896
|
import os
|
|
815
897
|
from codedna.integrations.github_bot import (
|
|
816
|
-
format_pr_comment, post_or_update_comment,
|
|
898
|
+
format_pr_comment, post_or_update_comment, github_actions_pr_bilgisi,
|
|
817
899
|
)
|
|
818
900
|
from codedna.tech_debt import calculate_repo_debt
|
|
819
901
|
|
|
@@ -830,7 +912,7 @@ def pr_comment(
|
|
|
830
912
|
target_pr = pr
|
|
831
913
|
|
|
832
914
|
if not target_repo or not target_pr:
|
|
833
|
-
info =
|
|
915
|
+
info = github_actions_pr_bilgisi()
|
|
834
916
|
if info:
|
|
835
917
|
auto_repo, auto_pr = info
|
|
836
918
|
target_repo = target_repo or auto_repo
|
|
@@ -1130,13 +1212,13 @@ def interview_list(
|
|
|
1130
1212
|
table.add_column("Notes", min_width=20)
|
|
1131
1213
|
|
|
1132
1214
|
for o in sessions:
|
|
1133
|
-
score_str = f"{o['
|
|
1215
|
+
score_str = f"{o['score']:.1f}/5" if o["score"] is not None else "[dim]—[/dim]"
|
|
1134
1216
|
table.add_row(
|
|
1135
1217
|
str(o["id"]),
|
|
1136
|
-
o["candidate"] or
|
|
1137
|
-
o["start_time"] or
|
|
1218
|
+
o["candidate"] or "?",
|
|
1219
|
+
o["start_time"] or "?",
|
|
1138
1220
|
score_str,
|
|
1139
|
-
(o["notes"] or
|
|
1221
|
+
(o["notes"] or "")[:30],
|
|
1140
1222
|
)
|
|
1141
1223
|
|
|
1142
1224
|
console.print()
|
|
@@ -1188,7 +1270,7 @@ def bus_factor(
|
|
|
1188
1270
|
from codedna.bus_factor import calculate_bus_factor, get_at_risk_files, _LARGE_REPO_THRESHOLD
|
|
1189
1271
|
from codedna.plan import is_feature_available
|
|
1190
1272
|
|
|
1191
|
-
|
|
1273
|
+
# Plan check
|
|
1192
1274
|
if not is_feature_available("bus_factor"):
|
|
1193
1275
|
console.print(
|
|
1194
1276
|
Panel(
|
|
@@ -2041,6 +2123,21 @@ def doctor(
|
|
|
2041
2123
|
console.print()
|
|
2042
2124
|
|
|
2043
2125
|
|
|
2126
|
+
# ---------------------------------------------------------------------------
|
|
2127
|
+
# codedna ask (natural language → command, powered by Needle)
|
|
2128
|
+
# ---------------------------------------------------------------------------
|
|
2129
|
+
|
|
2130
|
+
|
|
2131
|
+
@app.command()
|
|
2132
|
+
def ask(
|
|
2133
|
+
query: list[str] = typer.Argument(..., help="What you want to do, in plain language."),
|
|
2134
|
+
dry_run: bool = typer.Option(False, "--dry-run", help="Show the command without running it."),
|
|
2135
|
+
) -> None:
|
|
2136
|
+
"""Run a CodeDNA command from a natural-language request (powered by Needle)."""
|
|
2137
|
+
from codedna.nl import route_command
|
|
2138
|
+
route_command(" ".join(query), dry_run=dry_run)
|
|
2139
|
+
|
|
2140
|
+
|
|
2044
2141
|
# ---------------------------------------------------------------------------
|
|
2045
2142
|
# codedna update (self-upgrade from PyPI)
|
|
2046
2143
|
# ---------------------------------------------------------------------------
|
|
@@ -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,200 @@
|
|
|
1
|
+
"""NatureCo Account (SSO) — one NatureCo account, ecosystem-wide.
|
|
2
|
+
|
|
3
|
+
Shares the same session file (``~/.natureco/auth.json``) with the NatureCo CLI
|
|
4
|
+
and Cupertino Terminal, so signing in once works everywhere. Built on the
|
|
5
|
+
natureco.me Supabase Auth REST API. Dependency-free (standard-library urllib);
|
|
6
|
+
the anon key is public (embedded in every client, not a secret).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import base64
|
|
12
|
+
import json
|
|
13
|
+
import os
|
|
14
|
+
import time
|
|
15
|
+
import urllib.error
|
|
16
|
+
import urllib.request
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from urllib.parse import parse_qs, urlparse
|
|
19
|
+
|
|
20
|
+
# natureco.me identity project — anon key is PUBLIC (shipped in clients, not secret)
|
|
21
|
+
SUPABASE_URL = os.environ.get("NATURECO_SUPABASE_URL", "https://mxnlehflfkesasclcldy.supabase.co")
|
|
22
|
+
SUPABASE_ANON = os.environ.get(
|
|
23
|
+
"NATURECO_SUPABASE_ANON",
|
|
24
|
+
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im14bmxlaGZsZmtlc2FzY2xjbGR5Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzY2NDA5MzEsImV4cCI6MjA5MjIxNjkzMX0.93aPOg6bVmgFaJvsM5jVZwiX2TTuFIyAzhP6BlhBkGU",
|
|
25
|
+
)
|
|
26
|
+
AUTH_BASE = f"{SUPABASE_URL}/auth/v1"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class NatureCoAuthError(Exception):
|
|
30
|
+
def __init__(self, message: str, status_code: int | None = None):
|
|
31
|
+
super().__init__(message)
|
|
32
|
+
self.status_code = status_code
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _auth_file() -> Path:
|
|
36
|
+
return Path.home() / ".natureco" / "auth.json"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def load_session() -> dict | None:
|
|
40
|
+
try:
|
|
41
|
+
return json.loads(_auth_file().read_text(encoding="utf-8"))
|
|
42
|
+
except Exception:
|
|
43
|
+
return None
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def save_session(session: dict) -> dict:
|
|
47
|
+
f = _auth_file()
|
|
48
|
+
f.parent.mkdir(parents=True, exist_ok=True)
|
|
49
|
+
f.write_text(json.dumps(session, indent=2), encoding="utf-8")
|
|
50
|
+
try:
|
|
51
|
+
os.chmod(f, 0o600)
|
|
52
|
+
except Exception:
|
|
53
|
+
pass
|
|
54
|
+
return session
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def clear_session() -> None:
|
|
58
|
+
try:
|
|
59
|
+
_auth_file().unlink()
|
|
60
|
+
except Exception:
|
|
61
|
+
pass
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _post(path: str, body: dict, access_token: str | None = None) -> dict:
|
|
65
|
+
headers = {"apikey": SUPABASE_ANON, "Content-Type": "application/json"}
|
|
66
|
+
if access_token:
|
|
67
|
+
headers["Authorization"] = f"Bearer {access_token}"
|
|
68
|
+
req = urllib.request.Request(
|
|
69
|
+
f"{AUTH_BASE}{path}", data=json.dumps(body).encode(), headers=headers, method="POST"
|
|
70
|
+
)
|
|
71
|
+
try:
|
|
72
|
+
with urllib.request.urlopen(req, timeout=30) as r:
|
|
73
|
+
return json.loads(r.read().decode())
|
|
74
|
+
except urllib.error.HTTPError as e:
|
|
75
|
+
try:
|
|
76
|
+
data = json.loads(e.read().decode())
|
|
77
|
+
except Exception:
|
|
78
|
+
data = {}
|
|
79
|
+
msg = data.get("error_description") or data.get("msg") or data.get("error") or f"Auth error ({e.code})"
|
|
80
|
+
raise NatureCoAuthError(msg, e.code) from e
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _shape(s: dict) -> dict:
|
|
84
|
+
expires_at = s.get("expires_at") or (int(time.time()) + s["expires_in"] if s.get("expires_in") else None)
|
|
85
|
+
user = s.get("user")
|
|
86
|
+
return {
|
|
87
|
+
"access_token": s.get("access_token"),
|
|
88
|
+
"refresh_token": s.get("refresh_token"),
|
|
89
|
+
"token_type": s.get("token_type", "bearer"),
|
|
90
|
+
"expires_at": expires_at,
|
|
91
|
+
"user": {"id": user["id"], "email": user["email"]} if user else None,
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _user_from_jwt(token: str) -> dict | None:
|
|
96
|
+
"""Decode the user out of the access token (no signature check — display only)."""
|
|
97
|
+
try:
|
|
98
|
+
part = token.split(".")[1]
|
|
99
|
+
part += "=" * (-len(part) % 4)
|
|
100
|
+
p = json.loads(base64.urlsafe_b64decode(part))
|
|
101
|
+
return {"id": p.get("sub"), "email": p.get("email")}
|
|
102
|
+
except Exception:
|
|
103
|
+
return None
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def login_with_password(email: str, password: str) -> dict:
|
|
107
|
+
return save_session(_shape(_post("/token?grant_type=password", {"email": email, "password": password})))
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def send_otp(email: str) -> dict:
|
|
111
|
+
"""Passwordless: send a code / login link to the email (existing account only)."""
|
|
112
|
+
_post("/otp", {"email": email, "create_user": False})
|
|
113
|
+
return {"sent": True, "email": email}
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def verify_otp(email: str, token: str) -> dict:
|
|
117
|
+
"""Verify the OTP code. Depending on the email template the verification type
|
|
118
|
+
may be 'email' or 'magiclink', so both are attempted."""
|
|
119
|
+
code = "".join(str(token).split())
|
|
120
|
+
try:
|
|
121
|
+
return save_session(_shape(_post("/verify", {"type": "email", "email": email, "token": code})))
|
|
122
|
+
except NatureCoAuthError as e1:
|
|
123
|
+
try:
|
|
124
|
+
return save_session(_shape(_post("/verify", {"type": "magiclink", "email": email, "token": code})))
|
|
125
|
+
except Exception:
|
|
126
|
+
raise e1
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def verify_link(link: str) -> dict:
|
|
130
|
+
"""Sign in from a magic login-link email. Handles both the implicit flow
|
|
131
|
+
(access_token in the URL fragment) and the token_hash flow."""
|
|
132
|
+
u = urlparse(link.strip())
|
|
133
|
+
frag = parse_qs(u.fragment)
|
|
134
|
+
q = parse_qs(u.query)
|
|
135
|
+
|
|
136
|
+
def pick(k: str):
|
|
137
|
+
return (frag.get(k) or q.get(k) or [None])[0]
|
|
138
|
+
|
|
139
|
+
access_token = pick("access_token")
|
|
140
|
+
if access_token:
|
|
141
|
+
return save_session(_shape({
|
|
142
|
+
"access_token": access_token,
|
|
143
|
+
"refresh_token": pick("refresh_token"),
|
|
144
|
+
"token_type": pick("token_type") or "bearer",
|
|
145
|
+
"expires_at": int(pick("expires_at") or 0) or None,
|
|
146
|
+
"expires_in": int(pick("expires_in") or 0) or None,
|
|
147
|
+
"user": _user_from_jwt(access_token),
|
|
148
|
+
}))
|
|
149
|
+
token_hash = pick("token_hash") or pick("token")
|
|
150
|
+
typ = pick("type") or "magiclink"
|
|
151
|
+
if not token_hash:
|
|
152
|
+
raise NatureCoAuthError("No verification token found in the link")
|
|
153
|
+
return save_session(_shape(_post("/verify", {"type": typ, "token_hash": token_hash})))
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def refresh() -> dict:
|
|
157
|
+
s = load_session()
|
|
158
|
+
if not s or not s.get("refresh_token"):
|
|
159
|
+
raise NatureCoAuthError("Not signed in", 401)
|
|
160
|
+
return save_session(_shape(_post("/token?grant_type=refresh_token", {"refresh_token": s["refresh_token"]})))
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def get_access_token() -> str | None:
|
|
164
|
+
s = load_session()
|
|
165
|
+
if not s:
|
|
166
|
+
return None
|
|
167
|
+
if s.get("expires_at") and time.time() > s["expires_at"] - 60:
|
|
168
|
+
try:
|
|
169
|
+
s = refresh()
|
|
170
|
+
except Exception:
|
|
171
|
+
return None
|
|
172
|
+
return s.get("access_token") if s else None
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def whoami() -> dict | None:
|
|
176
|
+
token = get_access_token()
|
|
177
|
+
if not token:
|
|
178
|
+
return None
|
|
179
|
+
req = urllib.request.Request(
|
|
180
|
+
f"{AUTH_BASE}/user", headers={"apikey": SUPABASE_ANON, "Authorization": f"Bearer {token}"}
|
|
181
|
+
)
|
|
182
|
+
try:
|
|
183
|
+
with urllib.request.urlopen(req, timeout=30) as r:
|
|
184
|
+
return json.loads(r.read().decode())
|
|
185
|
+
except Exception:
|
|
186
|
+
return None
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def is_logged_in() -> bool:
|
|
190
|
+
s = load_session()
|
|
191
|
+
return bool(s and s.get("access_token"))
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def current_email() -> str | None:
|
|
195
|
+
s = load_session()
|
|
196
|
+
return s.get("user", {}).get("email") if s and s.get("user") else None
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def logout() -> None:
|
|
200
|
+
clear_session()
|
|
@@ -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
|
|
|
@@ -141,7 +142,10 @@ def scan_repository(
|
|
|
141
142
|
if file.suffix.lower() not in supported_extensions:
|
|
142
143
|
continue
|
|
143
144
|
|
|
144
|
-
|
|
145
|
+
# Use POSIX (forward-slash) paths so they match `git ls-files` output on
|
|
146
|
+
# Windows too (relative_to() yields backslashes there → never matched the
|
|
147
|
+
# tracked set → scan returned 0 files on Windows).
|
|
148
|
+
relative_path = file.relative_to(root).as_posix()
|
|
145
149
|
|
|
146
150
|
# Key fix: when a Git repo is present, scan ONLY git-tracked files.
|
|
147
151
|
# This automatically excludes all build/generated files in .gitignore.
|
|
@@ -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.3"
|
|
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,15 @@ dependencies = [
|
|
|
50
50
|
dashboard = [
|
|
51
51
|
"next-cmd>=0.5.0", # Next.js process management
|
|
52
52
|
]
|
|
53
|
+
nl = [
|
|
54
|
+
"huggingface_hub",
|
|
55
|
+
"pyarrow>=14,<18",
|
|
56
|
+
]
|
|
53
57
|
dev = [
|
|
54
|
-
"pytest>=
|
|
55
|
-
"pytest-cov>=
|
|
56
|
-
"black>=
|
|
57
|
-
"ruff>=0.
|
|
58
|
+
"pytest>=9.1.1",
|
|
59
|
+
"pytest-cov>=7.1.0",
|
|
60
|
+
"black>=26.5.1",
|
|
61
|
+
"ruff>=0.15.19",
|
|
58
62
|
]
|
|
59
63
|
|
|
60
64
|
[project.urls]
|
|
@@ -66,6 +70,9 @@ Changelog = "https://github.com/natureco-official/codedna/blob/main/CHANGELOG.md
|
|
|
66
70
|
[project.scripts]
|
|
67
71
|
codedna = "codedna.cli:main"
|
|
68
72
|
|
|
73
|
+
[tool.hatch.metadata]
|
|
74
|
+
allow-direct-references = true
|
|
75
|
+
|
|
69
76
|
[build-system]
|
|
70
77
|
requires = ["hatchling"]
|
|
71
78
|
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
|