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.
Files changed (94) hide show
  1. {codedna-0.9.1 → codedna-0.9.3}/PKG-INFO +8 -5
  2. {codedna-0.9.1 → codedna-0.9.3}/codedna/__init__.py +1 -1
  3. {codedna-0.9.1 → codedna-0.9.3}/codedna/analyzer.py +0 -2
  4. {codedna-0.9.1 → codedna-0.9.3}/codedna/api.py +1 -1
  5. {codedna-0.9.1 → codedna-0.9.3}/codedna/cli.py +108 -11
  6. {codedna-0.9.1 → codedna-0.9.3}/codedna/db.py +11 -5
  7. codedna-0.9.3/codedna/natureco_account.py +200 -0
  8. codedna-0.9.3/codedna/nl.py +234 -0
  9. codedna-0.9.3/codedna/nl_tools.json +127 -0
  10. {codedna-0.9.1 → codedna-0.9.3}/codedna/scorer.py +6 -2
  11. {codedna-0.9.1 → codedna-0.9.3}/codedna/tech_debt.py +1 -1
  12. {codedna-0.9.1 → codedna-0.9.3}/pyproject.toml +12 -5
  13. {codedna-0.9.1 → codedna-0.9.3}/.gitignore +0 -0
  14. {codedna-0.9.1 → codedna-0.9.3}/LICENSE +0 -0
  15. {codedna-0.9.1 → codedna-0.9.3}/README.md +0 -0
  16. {codedna-0.9.1 → codedna-0.9.3}/codedna/ai.py +0 -0
  17. {codedna-0.9.1 → codedna-0.9.3}/codedna/ai_box.py +0 -0
  18. {codedna-0.9.1 → codedna-0.9.3}/codedna/ai_fingerprint.py +0 -0
  19. {codedna-0.9.1 → codedna-0.9.3}/codedna/auth.py +0 -0
  20. {codedna-0.9.1 → codedna-0.9.3}/codedna/bus_factor.py +0 -0
  21. {codedna-0.9.1 → codedna-0.9.3}/codedna/demo.py +0 -0
  22. {codedna-0.9.1 → codedna-0.9.3}/codedna/git_hook.py +0 -0
  23. {codedna-0.9.1 → codedna-0.9.3}/codedna/integrations/__init__.py +0 -0
  24. {codedna-0.9.1 → codedna-0.9.3}/codedna/integrations/github_bot.py +0 -0
  25. {codedna-0.9.1 → codedna-0.9.3}/codedna/integrations/jira.py +0 -0
  26. {codedna-0.9.1 → codedna-0.9.3}/codedna/integrations/lemonsqueezy.py +0 -0
  27. {codedna-0.9.1 → codedna-0.9.3}/codedna/interview.py +0 -0
  28. {codedna-0.9.1 → codedna-0.9.3}/codedna/onboarding.py +0 -0
  29. {codedna-0.9.1 → codedna-0.9.3}/codedna/plan.py +0 -0
  30. {codedna-0.9.1 → codedna-0.9.3}/codedna/protection.py +0 -0
  31. {codedna-0.9.1 → codedna-0.9.3}/codedna/rate_limit.py +0 -0
  32. {codedna-0.9.1 → codedna-0.9.3}/codedna/sprint_health.py +0 -0
  33. {codedna-0.9.1 → codedna-0.9.3}/codedna/survey.py +0 -0
  34. {codedna-0.9.1 → codedna-0.9.3}/codedna/webhooks.py +0 -0
  35. {codedna-0.9.1 → codedna-0.9.3}/dashboard/.env.local.example +0 -0
  36. {codedna-0.9.1 → codedna-0.9.3}/dashboard/.gitignore +0 -0
  37. {codedna-0.9.1 → codedna-0.9.3}/dashboard/Dockerfile +0 -0
  38. {codedna-0.9.1 → codedna-0.9.3}/dashboard/README.md +0 -0
  39. {codedna-0.9.1 → codedna-0.9.3}/dashboard/app/ai-compare/page.tsx +0 -0
  40. {codedna-0.9.1 → codedna-0.9.3}/dashboard/app/api/auth/login/route.ts +0 -0
  41. {codedna-0.9.1 → codedna-0.9.3}/dashboard/app/api/auth/logout/route.ts +0 -0
  42. {codedna-0.9.1 → codedna-0.9.3}/dashboard/app/api/auth/me/route.ts +0 -0
  43. {codedna-0.9.1 → codedna-0.9.3}/dashboard/app/api/auth/register/route.ts +0 -0
  44. {codedna-0.9.1 → codedna-0.9.3}/dashboard/app/api/billing/checkout/route.ts +0 -0
  45. {codedna-0.9.1 → codedna-0.9.3}/dashboard/app/billing/page.tsx +0 -0
  46. {codedna-0.9.1 → codedna-0.9.3}/dashboard/app/bus-factor/page.tsx +0 -0
  47. {codedna-0.9.1 → codedna-0.9.3}/dashboard/app/commits/page.tsx +0 -0
  48. {codedna-0.9.1 → codedna-0.9.3}/dashboard/app/debt/page.tsx +0 -0
  49. {codedna-0.9.1 → codedna-0.9.3}/dashboard/app/favicon.ico +0 -0
  50. {codedna-0.9.1 → codedna-0.9.3}/dashboard/app/feedback/page.tsx +0 -0
  51. {codedna-0.9.1 → codedna-0.9.3}/dashboard/app/files/page.tsx +0 -0
  52. {codedna-0.9.1 → codedna-0.9.3}/dashboard/app/fonts/GeistMonoVF.woff +0 -0
  53. {codedna-0.9.1 → codedna-0.9.3}/dashboard/app/fonts/GeistVF.woff +0 -0
  54. {codedna-0.9.1 → codedna-0.9.3}/dashboard/app/globals.css +0 -0
  55. {codedna-0.9.1 → codedna-0.9.3}/dashboard/app/interview/page.tsx +0 -0
  56. {codedna-0.9.1 → codedna-0.9.3}/dashboard/app/layout.tsx +0 -0
  57. {codedna-0.9.1 → codedna-0.9.3}/dashboard/app/login/page.tsx +0 -0
  58. {codedna-0.9.1 → codedna-0.9.3}/dashboard/app/onboarding/page.tsx +0 -0
  59. {codedna-0.9.1 → codedna-0.9.3}/dashboard/app/page.tsx +0 -0
  60. {codedna-0.9.1 → codedna-0.9.3}/dashboard/app/pricing/page.tsx +0 -0
  61. {codedna-0.9.1 → codedna-0.9.3}/dashboard/app/protected/page.tsx +0 -0
  62. {codedna-0.9.1 → codedna-0.9.3}/dashboard/app/register/page.tsx +0 -0
  63. {codedna-0.9.1 → codedna-0.9.3}/dashboard/app/repos/page.tsx +0 -0
  64. {codedna-0.9.1 → codedna-0.9.3}/dashboard/app/settings/integrations/page.tsx +0 -0
  65. {codedna-0.9.1 → codedna-0.9.3}/dashboard/app/sprints/page.tsx +0 -0
  66. {codedna-0.9.1 → codedna-0.9.3}/dashboard/app/trends/page.tsx +0 -0
  67. {codedna-0.9.1 → codedna-0.9.3}/dashboard/components/AuthProvider.tsx +0 -0
  68. {codedna-0.9.1 → codedna-0.9.3}/dashboard/components/ClientProviders.tsx +0 -0
  69. {codedna-0.9.1 → codedna-0.9.3}/dashboard/components/CommitTable.tsx +0 -0
  70. {codedna-0.9.1 → codedna-0.9.3}/dashboard/components/CostInfoTooltip.tsx +0 -0
  71. {codedna-0.9.1 → codedna-0.9.3}/dashboard/components/ErrorBanner.tsx +0 -0
  72. {codedna-0.9.1 → codedna-0.9.3}/dashboard/components/FeatureGate.tsx +0 -0
  73. {codedna-0.9.1 → codedna-0.9.3}/dashboard/components/FileHeatmap.tsx +0 -0
  74. {codedna-0.9.1 → codedna-0.9.3}/dashboard/components/LanguageSwitcher.tsx +0 -0
  75. {codedna-0.9.1 → codedna-0.9.3}/dashboard/components/NavClient.tsx +0 -0
  76. {codedna-0.9.1 → codedna-0.9.3}/dashboard/components/OverviewTitles.tsx +0 -0
  77. {codedna-0.9.1 → codedna-0.9.3}/dashboard/components/QuickInsights.tsx +0 -0
  78. {codedna-0.9.1 → codedna-0.9.3}/dashboard/components/RiskBadge.tsx +0 -0
  79. {codedna-0.9.1 → codedna-0.9.3}/dashboard/components/SummaryCards.tsx +0 -0
  80. {codedna-0.9.1 → codedna-0.9.3}/dashboard/components/UnderstandingChart.tsx +0 -0
  81. {codedna-0.9.1 → codedna-0.9.3}/dashboard/components/UpgradeBanner.tsx +0 -0
  82. {codedna-0.9.1 → codedna-0.9.3}/dashboard/lib/api.ts +0 -0
  83. {codedna-0.9.1 → codedna-0.9.3}/dashboard/lib/auth.ts +0 -0
  84. {codedna-0.9.1 → codedna-0.9.3}/dashboard/lib/i18n/en.ts +0 -0
  85. {codedna-0.9.1 → codedna-0.9.3}/dashboard/lib/i18n/index.tsx +0 -0
  86. {codedna-0.9.1 → codedna-0.9.3}/dashboard/lib/i18n/tr.ts +0 -0
  87. {codedna-0.9.1 → codedna-0.9.3}/dashboard/lib/plan.ts +0 -0
  88. {codedna-0.9.1 → codedna-0.9.3}/dashboard/next-env.d.ts +0 -0
  89. {codedna-0.9.1 → codedna-0.9.3}/dashboard/next.config.mjs +0 -0
  90. {codedna-0.9.1 → codedna-0.9.3}/dashboard/package-lock.json +0 -0
  91. {codedna-0.9.1 → codedna-0.9.3}/dashboard/package.json +0 -0
  92. {codedna-0.9.1 → codedna-0.9.3}/dashboard/postcss.config.mjs +0 -0
  93. {codedna-0.9.1 → codedna-0.9.3}/dashboard/tailwind.config.ts +0 -0
  94. {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.1
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>=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">
@@ -1,4 +1,4 @@
1
1
  """CodeDNA — AI Code Transparency Tool."""
2
2
 
3
- __version__ = "0.9.1"
3
+ __version__ = "0.9.3"
4
4
  __app_name__ = "codedna"
@@ -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
@@ -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
- console.print()
145
- console.print("[bold cyan]🧬 CodeDNA[/bold cyan] — Scanning repo...\n")
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
- with console.status("[dim]Analyzing files...[/dim]"):
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, github_actions_pr_infosi,
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 = github_actions_pr_infosi()
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['skor']:.1f}/5" if o["skor"] is not None else "[dim]—[/dim]"
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 o.get("aday") or "?",
1137
- o["start_time"] or o.get("start_date") or "?",
1218
+ o["candidate"] or "?",
1219
+ o["start_time"] or "?",
1138
1220
  score_str,
1139
- (o["notes"] or o.get("notlar") or "")[:30],
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
- # Plan check
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
- 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,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
- relative_path = str(file.relative_to(root))
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.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.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>=8.0.0",
55
- "pytest-cov>=4.0.0",
56
- "black>=24.0.0",
57
- "ruff>=0.1.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