codedna 0.8.4__tar.gz → 0.9.0__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.8.4 → codedna-0.9.0}/PKG-INFO +1 -1
- {codedna-0.8.4 → codedna-0.9.0}/codedna/__init__.py +1 -1
- {codedna-0.8.4 → codedna-0.9.0}/codedna/analyzer.py +161 -0
- {codedna-0.8.4 → codedna-0.9.0}/codedna/api.py +174 -0
- {codedna-0.8.4 → codedna-0.9.0}/codedna/cli.py +401 -2
- {codedna-0.8.4 → codedna-0.9.0}/codedna/db.py +126 -0
- codedna-0.9.0/codedna/webhooks.py +138 -0
- codedna-0.9.0/dashboard/Dockerfile +28 -0
- codedna-0.9.0/dashboard/app/feedback/page.tsx +209 -0
- codedna-0.9.0/dashboard/app/repos/page.tsx +179 -0
- codedna-0.9.0/dashboard/app/trends/page.tsx +202 -0
- {codedna-0.8.4 → codedna-0.9.0}/dashboard/components/NavClient.tsx +3 -0
- {codedna-0.8.4 → codedna-0.9.0}/dashboard/lib/i18n/en.ts +43 -0
- {codedna-0.8.4 → codedna-0.9.0}/dashboard/lib/i18n/tr.ts +40 -0
- {codedna-0.8.4 → codedna-0.9.0}/pyproject.toml +1 -1
- {codedna-0.8.4 → codedna-0.9.0}/.gitignore +0 -0
- {codedna-0.8.4 → codedna-0.9.0}/LICENSE +0 -0
- {codedna-0.8.4 → codedna-0.9.0}/README.md +0 -0
- {codedna-0.8.4 → codedna-0.9.0}/codedna/ai.py +0 -0
- {codedna-0.8.4 → codedna-0.9.0}/codedna/ai_box.py +0 -0
- {codedna-0.8.4 → codedna-0.9.0}/codedna/ai_fingerprint.py +0 -0
- {codedna-0.8.4 → codedna-0.9.0}/codedna/auth.py +0 -0
- {codedna-0.8.4 → codedna-0.9.0}/codedna/bus_factor.py +0 -0
- {codedna-0.8.4 → codedna-0.9.0}/codedna/demo.py +0 -0
- {codedna-0.8.4 → codedna-0.9.0}/codedna/git_hook.py +0 -0
- {codedna-0.8.4 → codedna-0.9.0}/codedna/integrations/__init__.py +0 -0
- {codedna-0.8.4 → codedna-0.9.0}/codedna/integrations/github_bot.py +0 -0
- {codedna-0.8.4 → codedna-0.9.0}/codedna/integrations/jira.py +0 -0
- {codedna-0.8.4 → codedna-0.9.0}/codedna/integrations/lemonsqueezy.py +0 -0
- {codedna-0.8.4 → codedna-0.9.0}/codedna/interview.py +0 -0
- {codedna-0.8.4 → codedna-0.9.0}/codedna/onboarding.py +0 -0
- {codedna-0.8.4 → codedna-0.9.0}/codedna/plan.py +0 -0
- {codedna-0.8.4 → codedna-0.9.0}/codedna/protection.py +0 -0
- {codedna-0.8.4 → codedna-0.9.0}/codedna/rate_limit.py +0 -0
- {codedna-0.8.4 → codedna-0.9.0}/codedna/scorer.py +0 -0
- {codedna-0.8.4 → codedna-0.9.0}/codedna/sprint_health.py +0 -0
- {codedna-0.8.4 → codedna-0.9.0}/codedna/survey.py +0 -0
- {codedna-0.8.4 → codedna-0.9.0}/codedna/tech_debt.py +0 -0
- {codedna-0.8.4 → codedna-0.9.0}/dashboard/.env.local.example +0 -0
- {codedna-0.8.4 → codedna-0.9.0}/dashboard/.gitignore +0 -0
- {codedna-0.8.4 → codedna-0.9.0}/dashboard/README.md +0 -0
- {codedna-0.8.4 → codedna-0.9.0}/dashboard/app/ai-compare/page.tsx +0 -0
- {codedna-0.8.4 → codedna-0.9.0}/dashboard/app/api/auth/login/route.ts +0 -0
- {codedna-0.8.4 → codedna-0.9.0}/dashboard/app/api/auth/logout/route.ts +0 -0
- {codedna-0.8.4 → codedna-0.9.0}/dashboard/app/api/auth/me/route.ts +0 -0
- {codedna-0.8.4 → codedna-0.9.0}/dashboard/app/api/auth/register/route.ts +0 -0
- {codedna-0.8.4 → codedna-0.9.0}/dashboard/app/api/billing/checkout/route.ts +0 -0
- {codedna-0.8.4 → codedna-0.9.0}/dashboard/app/billing/page.tsx +0 -0
- {codedna-0.8.4 → codedna-0.9.0}/dashboard/app/bus-factor/page.tsx +0 -0
- {codedna-0.8.4 → codedna-0.9.0}/dashboard/app/commits/page.tsx +0 -0
- {codedna-0.8.4 → codedna-0.9.0}/dashboard/app/debt/page.tsx +0 -0
- {codedna-0.8.4 → codedna-0.9.0}/dashboard/app/favicon.ico +0 -0
- {codedna-0.8.4 → codedna-0.9.0}/dashboard/app/files/page.tsx +0 -0
- {codedna-0.8.4 → codedna-0.9.0}/dashboard/app/fonts/GeistMonoVF.woff +0 -0
- {codedna-0.8.4 → codedna-0.9.0}/dashboard/app/fonts/GeistVF.woff +0 -0
- {codedna-0.8.4 → codedna-0.9.0}/dashboard/app/globals.css +0 -0
- {codedna-0.8.4 → codedna-0.9.0}/dashboard/app/interview/page.tsx +0 -0
- {codedna-0.8.4 → codedna-0.9.0}/dashboard/app/layout.tsx +0 -0
- {codedna-0.8.4 → codedna-0.9.0}/dashboard/app/login/page.tsx +0 -0
- {codedna-0.8.4 → codedna-0.9.0}/dashboard/app/onboarding/page.tsx +0 -0
- {codedna-0.8.4 → codedna-0.9.0}/dashboard/app/page.tsx +0 -0
- {codedna-0.8.4 → codedna-0.9.0}/dashboard/app/pricing/page.tsx +0 -0
- {codedna-0.8.4 → codedna-0.9.0}/dashboard/app/protected/page.tsx +0 -0
- {codedna-0.8.4 → codedna-0.9.0}/dashboard/app/register/page.tsx +0 -0
- {codedna-0.8.4 → codedna-0.9.0}/dashboard/app/settings/integrations/page.tsx +0 -0
- {codedna-0.8.4 → codedna-0.9.0}/dashboard/app/sprints/page.tsx +0 -0
- {codedna-0.8.4 → codedna-0.9.0}/dashboard/components/AuthProvider.tsx +0 -0
- {codedna-0.8.4 → codedna-0.9.0}/dashboard/components/ClientProviders.tsx +0 -0
- {codedna-0.8.4 → codedna-0.9.0}/dashboard/components/CommitTable.tsx +0 -0
- {codedna-0.8.4 → codedna-0.9.0}/dashboard/components/CostInfoTooltip.tsx +0 -0
- {codedna-0.8.4 → codedna-0.9.0}/dashboard/components/ErrorBanner.tsx +0 -0
- {codedna-0.8.4 → codedna-0.9.0}/dashboard/components/FeatureGate.tsx +0 -0
- {codedna-0.8.4 → codedna-0.9.0}/dashboard/components/FileHeatmap.tsx +0 -0
- {codedna-0.8.4 → codedna-0.9.0}/dashboard/components/LanguageSwitcher.tsx +0 -0
- {codedna-0.8.4 → codedna-0.9.0}/dashboard/components/OverviewTitles.tsx +0 -0
- {codedna-0.8.4 → codedna-0.9.0}/dashboard/components/QuickInsights.tsx +0 -0
- {codedna-0.8.4 → codedna-0.9.0}/dashboard/components/RiskBadge.tsx +0 -0
- {codedna-0.8.4 → codedna-0.9.0}/dashboard/components/SummaryCards.tsx +0 -0
- {codedna-0.8.4 → codedna-0.9.0}/dashboard/components/UnderstandingChart.tsx +0 -0
- {codedna-0.8.4 → codedna-0.9.0}/dashboard/components/UpgradeBanner.tsx +0 -0
- {codedna-0.8.4 → codedna-0.9.0}/dashboard/lib/api.ts +0 -0
- {codedna-0.8.4 → codedna-0.9.0}/dashboard/lib/auth.ts +0 -0
- {codedna-0.8.4 → codedna-0.9.0}/dashboard/lib/i18n/index.tsx +0 -0
- {codedna-0.8.4 → codedna-0.9.0}/dashboard/lib/plan.ts +0 -0
- {codedna-0.8.4 → codedna-0.9.0}/dashboard/next-env.d.ts +0 -0
- {codedna-0.8.4 → codedna-0.9.0}/dashboard/next.config.mjs +0 -0
- {codedna-0.8.4 → codedna-0.9.0}/dashboard/package-lock.json +0 -0
- {codedna-0.8.4 → codedna-0.9.0}/dashboard/package.json +0 -0
- {codedna-0.8.4 → codedna-0.9.0}/dashboard/postcss.config.mjs +0 -0
- {codedna-0.8.4 → codedna-0.9.0}/dashboard/tailwind.config.ts +0 -0
- {codedna-0.8.4 → codedna-0.9.0}/dashboard/tsconfig.json +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codedna
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.9.0
|
|
4
4
|
Summary: AI Code Transparency Tool - detect AI-written code and measure developer understanding
|
|
5
5
|
Project-URL: Homepage, https://codedna.dev
|
|
6
6
|
Project-URL: Repository, https://github.com/natureco-official/codedna
|
|
@@ -7,6 +7,41 @@ from dataclasses import dataclass, field
|
|
|
7
7
|
from pathlib import Path
|
|
8
8
|
from typing import Optional
|
|
9
9
|
|
|
10
|
+
|
|
11
|
+
CONVENTIONAL_COMMIT_PATTERNS = {
|
|
12
|
+
"feat": "Feature",
|
|
13
|
+
"fix": "Bug Fix",
|
|
14
|
+
"docs": "Documentation",
|
|
15
|
+
"refactor": "Refactor",
|
|
16
|
+
"style": "Style",
|
|
17
|
+
"test": "Test",
|
|
18
|
+
"chore": "Chore",
|
|
19
|
+
"ci": "CI",
|
|
20
|
+
"build": "Build",
|
|
21
|
+
"perf": "Performance",
|
|
22
|
+
"revert": "Revert",
|
|
23
|
+
"wip": "WIP",
|
|
24
|
+
"merge": "Merge",
|
|
25
|
+
"release": "Release",
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
COMMIT_QUALITY_MAP = {
|
|
29
|
+
"Feature": 4.5,
|
|
30
|
+
"Bug Fix": 4.0,
|
|
31
|
+
"Refactor": 3.5,
|
|
32
|
+
"Documentation": 3.5,
|
|
33
|
+
"Style": 2.0,
|
|
34
|
+
"Test": 4.0,
|
|
35
|
+
"Chore": 2.5,
|
|
36
|
+
"CI": 3.0,
|
|
37
|
+
"Build": 3.0,
|
|
38
|
+
"Performance": 4.5,
|
|
39
|
+
"Revert": 1.0,
|
|
40
|
+
"WIP": 1.5,
|
|
41
|
+
"Merge": 2.0,
|
|
42
|
+
"Release": 3.5,
|
|
43
|
+
}
|
|
44
|
+
|
|
10
45
|
import tree_sitter_python as tspython
|
|
11
46
|
import tree_sitter_javascript as tsjavascript
|
|
12
47
|
from tree_sitter import Language, Parser, Node
|
|
@@ -46,6 +81,9 @@ class FileAnalysisResult:
|
|
|
46
81
|
function_count: int = 0
|
|
47
82
|
unsupported: bool = False # kept for internal use
|
|
48
83
|
error: Optional[str] = None # kept for internal use
|
|
84
|
+
explanation: list[str] = field(default_factory=list)
|
|
85
|
+
commit_type: Optional[str] = None
|
|
86
|
+
commit_quality: float = 2.5
|
|
49
87
|
|
|
50
88
|
@property
|
|
51
89
|
def complexity_label(self) -> str:
|
|
@@ -210,9 +248,132 @@ def analyze_file(
|
|
|
210
248
|
# Calculate AI probability
|
|
211
249
|
result.ai_probability = _calculate_ai_probability(result)
|
|
212
250
|
|
|
251
|
+
# Generate explanation
|
|
252
|
+
result.explanation = explain_ai_score(result)
|
|
253
|
+
|
|
254
|
+
return result
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def analyze_commit_message(message: str) -> dict:
|
|
258
|
+
"""
|
|
259
|
+
Analyze a commit message and return:
|
|
260
|
+
- type: conventional commit type (feat, fix, etc.)
|
|
261
|
+
- category: human-readable category
|
|
262
|
+
- quality_score: estimated quality (0-5)
|
|
263
|
+
- has_ticket: whether it references a ticket (JIRA/GitHub issue)
|
|
264
|
+
- is_conventional: whether it follows conventional commit format
|
|
265
|
+
- scope: the scope if present (e.g. "auth" in "feat(auth): ...")
|
|
266
|
+
"""
|
|
267
|
+
result = {
|
|
268
|
+
"type": None,
|
|
269
|
+
"category": None,
|
|
270
|
+
"quality_score": 2.5,
|
|
271
|
+
"has_ticket": False,
|
|
272
|
+
"is_conventional": False,
|
|
273
|
+
"scope": None,
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if not message:
|
|
277
|
+
return result
|
|
278
|
+
|
|
279
|
+
first_line = message.strip().split("\n")[0]
|
|
280
|
+
|
|
281
|
+
# Ticket reference detection
|
|
282
|
+
ticket_patterns = [
|
|
283
|
+
r"[A-Z]+-\d+", # JIRA: PROJ-123
|
|
284
|
+
r"#\d+", # GitHub: #42
|
|
285
|
+
r"gh-\d+", # GitHub CLI: gh-42
|
|
286
|
+
]
|
|
287
|
+
for pat in ticket_patterns:
|
|
288
|
+
if re.search(pat, first_line, re.IGNORECASE):
|
|
289
|
+
result["has_ticket"] = True
|
|
290
|
+
break
|
|
291
|
+
|
|
292
|
+
# Conventional commit: type(scope): description
|
|
293
|
+
conv_match = re.match(
|
|
294
|
+
r"^(fix|feat|docs|refactor|style|test|chore|ci|build|perf|revert)"
|
|
295
|
+
r"(\(([^)]+)\))?\s*:\s*(.+)",
|
|
296
|
+
first_line,
|
|
297
|
+
re.IGNORECASE,
|
|
298
|
+
)
|
|
299
|
+
if conv_match:
|
|
300
|
+
result["is_conventional"] = True
|
|
301
|
+
result["type"] = conv_match.group(1).lower()
|
|
302
|
+
result["scope"] = conv_match.group(3)
|
|
303
|
+
raw_type = conv_match.group(1).lower()
|
|
304
|
+
category = CONVENTIONAL_COMMIT_PATTERNS.get(raw_type, "Unknown")
|
|
305
|
+
result["category"] = category
|
|
306
|
+
result["quality_score"] = COMMIT_QUALITY_MAP.get(category, 2.5)
|
|
307
|
+
|
|
308
|
+
# Bonus for having a scope
|
|
309
|
+
if result["scope"]:
|
|
310
|
+
result["quality_score"] = min(result["quality_score"] + 0.5, 5.0)
|
|
311
|
+
|
|
312
|
+
# Bonus for having a ticket reference
|
|
313
|
+
if result["has_ticket"]:
|
|
314
|
+
result["quality_score"] = min(result["quality_score"] + 0.5, 5.0)
|
|
315
|
+
else:
|
|
316
|
+
# Non-conventional message — estimate quality
|
|
317
|
+
words = len(first_line.split())
|
|
318
|
+
if words < 3:
|
|
319
|
+
result["quality_score"] = 1.0
|
|
320
|
+
result["category"] = "Vague"
|
|
321
|
+
elif first_line.startswith("Merge"):
|
|
322
|
+
result["category"] = "Merge"
|
|
323
|
+
result["quality_score"] = 2.0
|
|
324
|
+
elif "wip" in first_line.lower() or "work in progress" in first_line.lower():
|
|
325
|
+
result["category"] = "WIP"
|
|
326
|
+
result["quality_score"] = 1.5
|
|
327
|
+
elif words > 10:
|
|
328
|
+
result["quality_score"] = 3.5
|
|
329
|
+
result["category"] = "Descriptive"
|
|
330
|
+
else:
|
|
331
|
+
result["category"] = "Standard"
|
|
332
|
+
result["quality_score"] = 2.5
|
|
333
|
+
|
|
213
334
|
return result
|
|
214
335
|
|
|
215
336
|
|
|
337
|
+
def explain_ai_score(result: FileAnalysisResult) -> list[str]:
|
|
338
|
+
"""
|
|
339
|
+
Generate human-readable explanations for the AI probability score.
|
|
340
|
+
Returns a list of reasons why the file scored as it did.
|
|
341
|
+
"""
|
|
342
|
+
reasons: list[str] = []
|
|
343
|
+
|
|
344
|
+
if result.comment_ratio > 0.3:
|
|
345
|
+
pct = int(result.comment_ratio * 100)
|
|
346
|
+
reasons.append(
|
|
347
|
+
f"High comment ratio ({pct}%) — AI-generated code tends to over-comment (+0.20)"
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
if result.avg_function_length > 50:
|
|
351
|
+
reasons.append(
|
|
352
|
+
f"Long avg. function length ({result.avg_function_length:.0f} lines) — "
|
|
353
|
+
f"AI tends to produce larger blocks (+0.15)"
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
if result.single_commit_ratio > 0.7:
|
|
357
|
+
pct = int(result.single_commit_ratio * 100)
|
|
358
|
+
reasons.append(
|
|
359
|
+
f"High single-commit ratio ({pct}%) — bulk paste indicator (+0.30)"
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
if result.complexity_score > 10 and result.single_commit_ratio > 0.5:
|
|
363
|
+
reasons.append(
|
|
364
|
+
f"High complexity ({result.complexity_score:.0f}) combined with bulk change — "
|
|
365
|
+
f"complex AI-generated code pattern (+0.25)"
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
if result.function_count == 0:
|
|
369
|
+
reasons.append("No functions detected — may be a config/data file")
|
|
370
|
+
|
|
371
|
+
if not reasons:
|
|
372
|
+
reasons.append("All metrics within normal range — likely human-written code")
|
|
373
|
+
|
|
374
|
+
return reasons
|
|
375
|
+
|
|
376
|
+
|
|
216
377
|
def _calculate_ai_probability(result: FileAnalysisResult) -> float:
|
|
217
378
|
"""
|
|
218
379
|
Calculate a rule-based AI probability score (0.0 – 1.0).
|
|
@@ -16,9 +16,11 @@ from pydantic import BaseModel
|
|
|
16
16
|
|
|
17
17
|
from codedna import __version__
|
|
18
18
|
from codedna.db import (
|
|
19
|
+
export_all_data as _export_all_data,
|
|
19
20
|
get_commit_history,
|
|
20
21
|
get_db_path,
|
|
21
22
|
get_file_scores_for_commit,
|
|
23
|
+
import_data as _import_data,
|
|
22
24
|
init_db,
|
|
23
25
|
update_understanding_score,
|
|
24
26
|
)
|
|
@@ -576,6 +578,178 @@ def _build_html_report(
|
|
|
576
578
|
</html>"""
|
|
577
579
|
|
|
578
580
|
|
|
581
|
+
# ---------------------------------------------------------------------------
|
|
582
|
+
# Export/Import endpoints
|
|
583
|
+
# ---------------------------------------------------------------------------
|
|
584
|
+
|
|
585
|
+
@app.get("/export", tags=["Export"])
|
|
586
|
+
async def export_data_endpoint() -> dict:
|
|
587
|
+
"""Export all database data as JSON."""
|
|
588
|
+
db = _db_path()
|
|
589
|
+
init_db(db)
|
|
590
|
+
return _export_all_data(db_path=db)
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
@app.post("/import", tags=["Export"])
|
|
594
|
+
async def import_data_endpoint(data: dict) -> dict:
|
|
595
|
+
"""Import previously exported data."""
|
|
596
|
+
db = _db_path()
|
|
597
|
+
init_db(db)
|
|
598
|
+
counts = _import_data(data, db_path=db)
|
|
599
|
+
return {"status": "imported", **counts}
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
# ---------------------------------------------------------------------------
|
|
603
|
+
# Trend data endpoints
|
|
604
|
+
# ---------------------------------------------------------------------------
|
|
605
|
+
|
|
606
|
+
@app.get("/trends", tags=["Trends"])
|
|
607
|
+
async def trends(
|
|
608
|
+
limit: int = Query(30, ge=1, le=365, description="Number of days to return"),
|
|
609
|
+
) -> dict:
|
|
610
|
+
"""Return day-by-day trend data for AI score and understanding over time."""
|
|
611
|
+
db = _db_path()
|
|
612
|
+
init_db(db)
|
|
613
|
+
try:
|
|
614
|
+
from codedna.db import get_trend_data
|
|
615
|
+
rows = get_trend_data(db_path=db, limit=limit)
|
|
616
|
+
except Exception:
|
|
617
|
+
rows = []
|
|
618
|
+
return {"total_days": len(rows), "trends": rows}
|
|
619
|
+
|
|
620
|
+
|
|
621
|
+
@app.get("/trends/commits", tags=["Trends"])
|
|
622
|
+
async def commit_trends(
|
|
623
|
+
limit: int = Query(50, ge=1, le=500, description="Number of commits"),
|
|
624
|
+
) -> dict:
|
|
625
|
+
"""Return commit-level trend data (AI score, understanding per commit)."""
|
|
626
|
+
db = _db_path()
|
|
627
|
+
init_db(db)
|
|
628
|
+
commits = get_commit_history(limit=limit, db_path=db)
|
|
629
|
+
items = []
|
|
630
|
+
for c in commits:
|
|
631
|
+
files = get_file_scores_for_commit(c["commit_hash"], db_path=db)
|
|
632
|
+
ai_scores = [float(f["ai_probability"]) for f in files if f["ai_probability"] is not None]
|
|
633
|
+
avg_ai = sum(ai_scores) / len(ai_scores) if ai_scores else None
|
|
634
|
+
items.append({
|
|
635
|
+
"commit_hash": c["commit_hash"][:8] if c["commit_hash"] else "",
|
|
636
|
+
"author": c["author"],
|
|
637
|
+
"date": datetime.fromtimestamp(c["timestamp"]).strftime("%Y-%m-%d") if c["timestamp"] else None,
|
|
638
|
+
"avg_ai_score": round(avg_ai, 3) if avg_ai else None,
|
|
639
|
+
"understanding_score": round(float(c["understanding_score"]), 2) if c["understanding_score"] is not None else None,
|
|
640
|
+
})
|
|
641
|
+
return {"total": len(items), "trends": list(reversed(items))}
|
|
642
|
+
|
|
643
|
+
|
|
644
|
+
# ---------------------------------------------------------------------------
|
|
645
|
+
# Feedback endpoints
|
|
646
|
+
# ---------------------------------------------------------------------------
|
|
647
|
+
|
|
648
|
+
FEEDBACK_PATH = Path.home() / ".codedna" / "feedback.json"
|
|
649
|
+
|
|
650
|
+
|
|
651
|
+
@app.post("/feedback", tags=["Feedback"])
|
|
652
|
+
async def save_feedback(data: dict) -> dict:
|
|
653
|
+
"""Save user feedback on AI analysis accuracy."""
|
|
654
|
+
db = _db_path()
|
|
655
|
+
init_db(db)
|
|
656
|
+
feedback_file = FEEDBACK_PATH
|
|
657
|
+
|
|
658
|
+
feedback_file.parent.mkdir(parents=True, exist_ok=True)
|
|
659
|
+
existing = []
|
|
660
|
+
if feedback_file.exists():
|
|
661
|
+
try:
|
|
662
|
+
existing = json.loads(feedback_file.read_text())
|
|
663
|
+
except Exception:
|
|
664
|
+
existing = []
|
|
665
|
+
|
|
666
|
+
entry = {
|
|
667
|
+
"id": len(existing) + 1,
|
|
668
|
+
"timestamp": datetime.utcnow().isoformat(),
|
|
669
|
+
"file_path": data.get("file_path", ""),
|
|
670
|
+
"commit_hash": data.get("commit_hash", ""),
|
|
671
|
+
"ai_probability": data.get("ai_probability"),
|
|
672
|
+
"user_rating": data.get("rating"), # "correct", "incorrect", "unsure"
|
|
673
|
+
"user_note": data.get("note", ""),
|
|
674
|
+
}
|
|
675
|
+
existing.append(entry)
|
|
676
|
+
feedback_file.write_text(json.dumps(existing, indent=2))
|
|
677
|
+
return {"status": "saved", "id": entry["id"]}
|
|
678
|
+
|
|
679
|
+
|
|
680
|
+
@app.get("/feedback", tags=["Feedback"])
|
|
681
|
+
async def list_feedback(limit: int = Query(50, ge=1, le=500)) -> dict:
|
|
682
|
+
"""Return user feedback entries."""
|
|
683
|
+
feedback_file = FEEDBACK_PATH
|
|
684
|
+
if not feedback_file.exists():
|
|
685
|
+
return {"total": 0, "feedback": []}
|
|
686
|
+
try:
|
|
687
|
+
items = json.loads(feedback_file.read_text())
|
|
688
|
+
except Exception:
|
|
689
|
+
items = []
|
|
690
|
+
return {"total": len(items), "feedback": items[-limit:]}
|
|
691
|
+
|
|
692
|
+
|
|
693
|
+
# ---------------------------------------------------------------------------
|
|
694
|
+
# Multi-repo endpoints
|
|
695
|
+
# ---------------------------------------------------------------------------
|
|
696
|
+
|
|
697
|
+
MULTI_REPO_PATH = Path.home() / ".codedna" / "repos.json"
|
|
698
|
+
|
|
699
|
+
|
|
700
|
+
@app.get("/repos", tags=["Multi-Repo"])
|
|
701
|
+
async def list_repos() -> dict:
|
|
702
|
+
"""Return configured multi-repo list."""
|
|
703
|
+
repos_file = MULTI_REPO_PATH
|
|
704
|
+
if not repos_file.exists():
|
|
705
|
+
return {"total": 0, "repos": []}
|
|
706
|
+
try:
|
|
707
|
+
items = json.loads(repos_file.read_text())
|
|
708
|
+
except Exception:
|
|
709
|
+
items = []
|
|
710
|
+
return {"total": len(items), "repos": items}
|
|
711
|
+
|
|
712
|
+
|
|
713
|
+
@app.post("/repos", tags=["Multi-Repo"])
|
|
714
|
+
async def add_repo(data: dict) -> dict:
|
|
715
|
+
"""Add a repository to the multi-repo config."""
|
|
716
|
+
repos_file = MULTI_REPO_PATH
|
|
717
|
+
repos_file.parent.mkdir(parents=True, exist_ok=True)
|
|
718
|
+
existing = []
|
|
719
|
+
if repos_file.exists():
|
|
720
|
+
try:
|
|
721
|
+
existing = json.loads(repos_file.read_text())
|
|
722
|
+
except Exception:
|
|
723
|
+
existing = []
|
|
724
|
+
|
|
725
|
+
new_repo = {
|
|
726
|
+
"id": len(existing) + 1,
|
|
727
|
+
"name": data.get("name", ""),
|
|
728
|
+
"path": data.get("path", ""),
|
|
729
|
+
"added_at": datetime.utcnow().isoformat(),
|
|
730
|
+
}
|
|
731
|
+
existing.append(new_repo)
|
|
732
|
+
repos_file.write_text(json.dumps(existing, indent=2))
|
|
733
|
+
return {"status": "added", "repo": new_repo}
|
|
734
|
+
|
|
735
|
+
|
|
736
|
+
@app.delete("/repos/{repo_id}", tags=["Multi-Repo"])
|
|
737
|
+
async def remove_repo(repo_id: int) -> dict:
|
|
738
|
+
"""Remove a repository from the multi-repo config."""
|
|
739
|
+
repos_file = MULTI_REPO_PATH
|
|
740
|
+
if not repos_file.exists():
|
|
741
|
+
raise HTTPException(status_code=404, detail="No repos configured.")
|
|
742
|
+
try:
|
|
743
|
+
items = json.loads(repos_file.read_text())
|
|
744
|
+
except Exception:
|
|
745
|
+
items = []
|
|
746
|
+
filtered = [r for r in items if r.get("id") != repo_id]
|
|
747
|
+
if len(filtered) == len(items):
|
|
748
|
+
raise HTTPException(status_code=404, detail=f"Repo #{repo_id} not found.")
|
|
749
|
+
repos_file.write_text(json.dumps(filtered, indent=2))
|
|
750
|
+
return {"status": "removed", "repo_id": repo_id}
|
|
751
|
+
|
|
752
|
+
|
|
579
753
|
# ---------------------------------------------------------------------------
|
|
580
754
|
# Helper: plan 403 response
|
|
581
755
|
# ---------------------------------------------------------------------------
|