Spaces:
Sleeping
Sleeping
Update app/predictor.py
Browse files- app/predictor.py +190 -432
app/predictor.py
CHANGED
|
@@ -1,181 +1,177 @@
|
|
| 1 |
-
# for the loading of CODE_BERT model
|
| 2 |
-
|
| 3 |
import logging
|
|
|
|
| 4 |
import torch
|
| 5 |
import torch.nn.functional as F
|
| 6 |
from transformers import AutoModel, AutoTokenizer
|
| 7 |
-
from app.services.reviewer_service import AIReviewerService
|
| 8 |
|
| 9 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
logging.basicConfig(level=logging.INFO)
|
| 11 |
logger = logging.getLogger(__name__)
|
| 12 |
|
| 13 |
|
| 14 |
class CodeClassifier:
|
| 15 |
-
|
|
|
|
|
|
|
|
|
|
| 16 |
def __init__(self):
|
| 17 |
-
logger.info("⏳ Initializing AI Service...")
|
| 18 |
|
| 19 |
-
# Detect Hardware (Uses GPU on Victus, OR fallback if GPU is not available CPU/MPS)
|
| 20 |
self.device = "cuda" if torch.cuda.is_available() else "cpu"
|
| 21 |
if torch.backends.mps.is_available():
|
| 22 |
self.device = "mps"
|
| 23 |
|
| 24 |
logger.info(f"🚀 Running on device: {self.device}")
|
| 25 |
|
| 26 |
-
# This will download 'microsoft/codebert-base' from Hugging Face
|
| 27 |
-
# the first time it runs. It caches it locally afterwards.
|
| 28 |
try:
|
| 29 |
-
logger.info(
|
| 30 |
-
"📥 Loading CodeBERT Model (this may take a minute first time)..."
|
| 31 |
-
)
|
| 32 |
self.tokenizer = AutoTokenizer.from_pretrained("microsoft/codebert-base")
|
| 33 |
self.model = AutoModel.from_pretrained("microsoft/codebert-base").to(
|
| 34 |
self.device
|
| 35 |
)
|
| 36 |
-
logger.info("✅ CodeBERT Loaded Successfully!")
|
| 37 |
except Exception as e:
|
| 38 |
-
logger.error(f"❌ Failed to load model: {e}")
|
| 39 |
raise e
|
| 40 |
|
| 41 |
-
# Initialize Semantic Anchors for Classification
|
| 42 |
self.labels = {
|
| 43 |
-
"Frontend": "import react component from view styles css html dom window document state props effect",
|
| 44 |
-
"Backend": "import express nest controller service entity repository database sql mongoose route api async await req res dto",
|
| 45 |
-
"Security": "import auth passport jwt strategy bcrypt verify token secret guard password user login session middleware",
|
| 46 |
-
"DevOps": "docker build image container kubernetes yaml env port host volume deploy pipeline stage steps runs-on",
|
| 47 |
-
"Testing": "describe it expect test mock spy jest beforeall aftereach suite spec assert",
|
| 48 |
}
|
| 49 |
self.label_embeddings = self._precompute_label_embeddings()
|
| 50 |
|
| 51 |
def _get_embedding(self, text: str):
|
| 52 |
-
"""
|
| 53 |
-
Generates a 768-dim vector for the given text using CodeBERT.
|
| 54 |
-
"""
|
| 55 |
-
# Truncate text to avoid model errors (CodeBERT max is 512 tokens)
|
| 56 |
inputs = self.tokenizer(
|
| 57 |
text, return_tensors="pt", padding=True, truncation=True, max_length=512
|
| 58 |
).to(self.device)
|
| 59 |
with torch.no_grad():
|
| 60 |
outputs = self.model(**inputs)
|
| 61 |
-
# Use the [CLS] token embedding (index 0) as the sentence representation
|
| 62 |
return outputs.last_hidden_state[:, 0, :]
|
| 63 |
|
| 64 |
def _precompute_label_embeddings(self):
|
| 65 |
-
"""
|
| 66 |
-
|
| 67 |
-
"""
|
| 68 |
-
logger.info("🧠 Pre-computing semantic anchors for classification...")
|
| 69 |
embeddings = {}
|
| 70 |
for label, description in self.labels.items():
|
| 71 |
embeddings[label] = self._get_embedding(description)
|
| 72 |
return embeddings
|
| 73 |
|
| 74 |
-
# for pridicting our file name themes and then classiyfing them into related catagory
|
| 75 |
def predict(self, file_path: str, content: str = None) -> dict:
|
| 76 |
"""
|
| 77 |
-
Determines the
|
| 78 |
-
Returns: { "label": str, "confidence": float, "embedding":
|
| 79 |
"""
|
| 80 |
path = file_path.lower()
|
| 81 |
|
| 82 |
-
|
| 83 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
return {
|
| 85 |
"label": label,
|
| 86 |
"confidence": conf,
|
| 87 |
-
"embedding":
|
| 88 |
}
|
| 89 |
|
| 90 |
-
# 1. Fast Path:
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
# Security Indicators
|
| 123 |
-
if any(
|
| 124 |
-
x in path
|
| 125 |
-
for x in ["auth", "guard", "strategy", "jwt", "passport", "middleware"]
|
| 126 |
-
):
|
| 127 |
-
return result("Security")
|
| 128 |
-
|
| 129 |
-
# DevOps/Config Indicators
|
| 130 |
-
if any(
|
| 131 |
-
x in path
|
| 132 |
-
for x in ["docker", "k8s", "github/workflows", "tsconfig", "package.json"]
|
| 133 |
-
):
|
| 134 |
-
return result("DevOps")
|
| 135 |
-
|
| 136 |
-
# Testing Indicators
|
| 137 |
-
if any(x in path for x in ["test", "spec", "e2e", "jest"]):
|
| 138 |
-
return result("Testing")
|
| 139 |
-
|
| 140 |
-
# 2. Slow Path: AI-Powered Semantic Classification (High Recall)
|
| 141 |
-
# If the rules are unsure ("Generic"), we ask the model.
|
| 142 |
try:
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
) # Convert to list for JSON serialization
|
| 154 |
-
|
| 155 |
-
best_label = "Generic"
|
| 156 |
-
highest_score = -1.0
|
| 157 |
-
|
| 158 |
-
for label, anchor_embedding in self.label_embeddings.items():
|
| 159 |
-
# Cosine Similarity: -1 to 1
|
| 160 |
-
score = F.cosine_similarity(
|
| 161 |
-
target_embedding_tensor, anchor_embedding
|
| 162 |
-
).item()
|
| 163 |
-
if score > highest_score:
|
| 164 |
-
highest_score = score
|
| 165 |
-
best_label = label
|
| 166 |
-
|
| 167 |
-
# Only accept the AI's opinion if it's somewhat confident
|
| 168 |
-
if highest_score > 0.25:
|
| 169 |
-
return result(best_label, highest_score, target_embedding_list)
|
| 170 |
-
|
| 171 |
-
return result("Generic", highest_score, target_embedding_list)
|
| 172 |
-
|
| 173 |
except Exception as e:
|
| 174 |
-
logger.error(f"
|
| 175 |
-
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 176 |
|
| 177 |
|
| 178 |
class GuideGenerator:
|
|
|
|
|
|
|
|
|
|
| 179 |
def __init__(self):
|
| 180 |
self.tech_stacks = {
|
| 181 |
"React": ["react", "jsx", "tsx", "next.config.js"],
|
|
@@ -188,10 +184,10 @@ class GuideGenerator:
|
|
| 188 |
"Django": ["django", "manage.py"],
|
| 189 |
"Flask": ["flask", "app.py"],
|
| 190 |
"Spring Boot": ["pom.xml", "build.gradle", "src/main/java"],
|
|
|
|
| 191 |
"Go": ["go.mod", "main.go"],
|
| 192 |
"Rust": ["Cargo.toml", "src/main.rs"],
|
| 193 |
}
|
| 194 |
-
|
| 195 |
self.tools = {
|
| 196 |
"Docker": ["Dockerfile", "docker-compose.yml"],
|
| 197 |
"Kubernetes": ["k8s", "helm", "charts/"],
|
|
@@ -202,11 +198,13 @@ class GuideGenerator:
|
|
| 202 |
"PostgreSQL": ["postgresql", "pg"],
|
| 203 |
"MongoDB": ["mongoose", "mongodb"],
|
| 204 |
"Redis": ["redis"],
|
|
|
|
|
|
|
|
|
|
| 205 |
}
|
| 206 |
|
| 207 |
def detect_stack(self, files: list[str]) -> dict:
|
| 208 |
detected = {"languages": set(), "frameworks": set(), "tools": set()}
|
| 209 |
-
|
| 210 |
for file in files:
|
| 211 |
path = file.lower()
|
| 212 |
|
|
@@ -223,130 +221,29 @@ class GuideGenerator:
|
|
| 223 |
detected["languages"].add("Rust")
|
| 224 |
elif path.endswith(".java"):
|
| 225 |
detected["languages"].add("Java")
|
|
|
|
|
|
|
| 226 |
|
| 227 |
# Frameworks
|
| 228 |
for framework, indicators in self.tech_stacks.items():
|
| 229 |
if any(ind in path for ind in indicators):
|
| 230 |
detected["frameworks"].add(framework)
|
| 231 |
|
| 232 |
-
# Tools
|
| 233 |
for tool, indicators in self.tools.items():
|
| 234 |
if any(ind in path for ind in indicators):
|
| 235 |
detected["tools"].add(tool)
|
| 236 |
|
| 237 |
return detected
|
| 238 |
|
| 239 |
-
def _generate_tree(self, files: list[str]) -> str:
|
| 240 |
-
"""
|
| 241 |
-
Generates a clean ASCII tree of the project structure with architectural annotations.
|
| 242 |
-
"""
|
| 243 |
-
tree = {}
|
| 244 |
-
relevant_files = []
|
| 245 |
-
|
| 246 |
-
# 1. Filter and normalize paths
|
| 247 |
-
for f in files:
|
| 248 |
-
parts = f.split("/")
|
| 249 |
-
|
| 250 |
-
# Skip noise
|
| 251 |
-
if any(
|
| 252 |
-
p
|
| 253 |
-
in [
|
| 254 |
-
"node_modules",
|
| 255 |
-
".git",
|
| 256 |
-
"__pycache__",
|
| 257 |
-
"dist",
|
| 258 |
-
"build",
|
| 259 |
-
".idea",
|
| 260 |
-
".vscode",
|
| 261 |
-
]
|
| 262 |
-
for p in parts
|
| 263 |
-
):
|
| 264 |
-
continue
|
| 265 |
-
if f.endswith(".DS_Store"):
|
| 266 |
-
continue
|
| 267 |
-
|
| 268 |
-
relevant_files.append(f)
|
| 269 |
-
|
| 270 |
-
# 2. Build nested dictionary
|
| 271 |
-
for path in relevant_files:
|
| 272 |
-
parts = path.split("/")
|
| 273 |
-
if len(parts) > 3:
|
| 274 |
-
parts = parts[:3]
|
| 275 |
-
|
| 276 |
-
current = tree
|
| 277 |
-
for part in parts:
|
| 278 |
-
current = current.setdefault(part, {})
|
| 279 |
-
|
| 280 |
-
# 3. Define Annotations
|
| 281 |
-
descriptions = {
|
| 282 |
-
"src": "Core application source code",
|
| 283 |
-
"app": "Main application logic",
|
| 284 |
-
"components": "Reusable UI components",
|
| 285 |
-
"pages": "Route/Page definitions",
|
| 286 |
-
"api": "API endpoints and services",
|
| 287 |
-
"utils": "Utility functions and helpers",
|
| 288 |
-
"lib": "External libraries and configurations",
|
| 289 |
-
"test": "Unit and integration tests",
|
| 290 |
-
"tests": "Test suites",
|
| 291 |
-
"docs": "Project documentation",
|
| 292 |
-
"public": "Static assets (images, fonts)",
|
| 293 |
-
"assets": "Static media files",
|
| 294 |
-
"server": "Backend server code",
|
| 295 |
-
"client": "Frontend client application",
|
| 296 |
-
"config": "Configuration files",
|
| 297 |
-
"scripts": "Build and maintenance scripts",
|
| 298 |
-
"prisma": "Database schema and migrations",
|
| 299 |
-
"graphql": "GraphQL definitions",
|
| 300 |
-
}
|
| 301 |
-
|
| 302 |
-
# 4. Render tree
|
| 303 |
-
lines = []
|
| 304 |
-
|
| 305 |
-
def render(node, prefix=""):
|
| 306 |
-
keys = sorted(node.keys())
|
| 307 |
-
|
| 308 |
-
# Priority sorting: put 'src', 'app', 'server', 'client' first
|
| 309 |
-
priority = ["src", "app", "client", "server", "public"]
|
| 310 |
-
keys.sort(key=lambda k: (0 if k in priority else 1, k))
|
| 311 |
-
|
| 312 |
-
if len(keys) > 12:
|
| 313 |
-
keys = keys[:12] + ["..."]
|
| 314 |
-
|
| 315 |
-
for i, key in enumerate(keys):
|
| 316 |
-
is_last = i == len(keys) - 1
|
| 317 |
-
connector = "└── " if is_last else "├── "
|
| 318 |
-
|
| 319 |
-
# Add annotation if available and it's a folder (has children)
|
| 320 |
-
comment = ""
|
| 321 |
-
if key in descriptions and isinstance(node[key], dict) and node[key]:
|
| 322 |
-
comment = f" # {descriptions[key]}"
|
| 323 |
-
|
| 324 |
-
lines.append(f"{prefix}{connector}{key}{comment}")
|
| 325 |
-
|
| 326 |
-
if isinstance(node.get(key), dict) and node[key]:
|
| 327 |
-
extension = " " if is_last else "│ "
|
| 328 |
-
render(node[key], prefix + extension)
|
| 329 |
-
|
| 330 |
-
render(tree)
|
| 331 |
-
return "\n".join(lines[:50])
|
| 332 |
-
|
| 333 |
def generate_markdown(self, repo_name: str, files: list[str]) -> str:
|
| 334 |
-
|
| 335 |
-
stats = {
|
| 336 |
-
"Frontend": 0,
|
| 337 |
-
"Backend": 0,
|
| 338 |
-
"Security": 0,
|
| 339 |
-
"DevOps": 0,
|
| 340 |
-
"Testing": 0,
|
| 341 |
-
"Generic": 0,
|
| 342 |
-
}
|
| 343 |
layer_map = {}
|
| 344 |
-
|
| 345 |
low_confidence_files = []
|
| 346 |
-
file_embeddings = {}
|
| 347 |
|
| 348 |
for f in files:
|
| 349 |
-
# We use CodeBERT's path-embedding capability here since we don't have content for all files
|
| 350 |
prediction = classifier.predict(f)
|
| 351 |
layer = prediction["label"]
|
| 352 |
confidence = prediction["confidence"]
|
|
@@ -357,32 +254,26 @@ class GuideGenerator:
|
|
| 357 |
if confidence < 0.4 and layer != "Generic":
|
| 358 |
low_confidence_files.append((f, confidence))
|
| 359 |
|
| 360 |
-
|
| 361 |
-
|
| 362 |
-
file_embeddings[f] = torch.tensor(prediction["embedding"])
|
| 363 |
|
| 364 |
total_files = len(files) if files else 1
|
| 365 |
primary_layer = max(stats, key=stats.get)
|
| 366 |
|
| 367 |
-
#
|
| 368 |
couplings = []
|
| 369 |
try:
|
| 370 |
-
|
| 371 |
-
# Limit to first 50 files for safety and performance
|
| 372 |
-
sample_paths = paths[:50]
|
| 373 |
-
|
| 374 |
for i in range(len(sample_paths)):
|
| 375 |
for j in range(i + 1, len(sample_paths)):
|
| 376 |
p1, p2 = sample_paths[i], sample_paths[j]
|
| 377 |
-
# Skip same folder
|
| 378 |
if p1.rsplit("/", 1)[0] == p2.rsplit("/", 1)[0]:
|
| 379 |
continue
|
| 380 |
|
| 381 |
-
# Ensure tensors are valid and on CPU for comparison
|
| 382 |
t1 = file_embeddings[p1].cpu()
|
| 383 |
t2 = file_embeddings[p2].cpu()
|
| 384 |
|
| 385 |
-
score = F.cosine_similarity(t1
|
| 386 |
if score > 0.88:
|
| 387 |
couplings.append((p1, p2, score))
|
| 388 |
except Exception as e:
|
|
@@ -391,11 +282,9 @@ class GuideGenerator:
|
|
| 391 |
couplings.sort(key=lambda x: x[2], reverse=True)
|
| 392 |
top_couplings = couplings[:5]
|
| 393 |
|
| 394 |
-
# Sort low confidence by lowest score
|
| 395 |
low_confidence_files.sort(key=lambda x: x[1])
|
| 396 |
top_refactors = low_confidence_files[:5]
|
| 397 |
|
| 398 |
-
# 2. Advanced Stack & Feature Detection
|
| 399 |
stack = self.detect_stack(files)
|
| 400 |
features = self._detect_features(files, stats)
|
| 401 |
dev_tools = self._detect_dev_tools(files)
|
|
@@ -404,7 +293,11 @@ class GuideGenerator:
|
|
| 404 |
run_cmd = "npm run dev"
|
| 405 |
test_cmd = "npm test"
|
| 406 |
|
| 407 |
-
if "
|
|
|
|
|
|
|
|
|
|
|
|
|
| 408 |
install_cmd = "pip install -r requirements.txt"
|
| 409 |
run_cmd = "python main.py"
|
| 410 |
test_cmd = "pytest"
|
|
@@ -412,41 +305,25 @@ class GuideGenerator:
|
|
| 412 |
install_cmd = "go mod download"
|
| 413 |
run_cmd = "go run main.go"
|
| 414 |
test_cmd = "go test ./..."
|
| 415 |
-
elif "Rust" in stack["languages"]:
|
| 416 |
-
install_cmd = "cargo build"
|
| 417 |
-
run_cmd = "cargo run"
|
| 418 |
-
test_cmd = "cargo test"
|
| 419 |
-
|
| 420 |
-
if "Django" in stack["frameworks"]:
|
| 421 |
-
run_cmd = "python manage.py runserver"
|
| 422 |
-
test_cmd = "python manage.py test"
|
| 423 |
-
elif "Spring Boot" in stack["frameworks"]:
|
| 424 |
-
install_cmd = "./mvnw install"
|
| 425 |
-
run_cmd = "./mvnw spring-boot:run"
|
| 426 |
-
if "Docker" in stack["tools"]:
|
| 427 |
-
run_cmd += "\n# Or using Docker\ndocker-compose up --build"
|
| 428 |
-
|
| 429 |
-
# 3. Assemble Guide
|
| 430 |
-
md = f"# {repo_name} Developer Guide\n\n"
|
| 431 |
|
|
|
|
|
|
|
| 432 |
md += "## AI Codebase Insights\n"
|
| 433 |
-
md +=
|
| 434 |
md += f"**Project DNA:** {self._get_project_dna(stats, total_files)}\n\n"
|
| 435 |
md += f"**Quality Check:** {self._get_testing_status(stats, total_files)}\n\n"
|
| 436 |
|
| 437 |
if top_refactors:
|
| 438 |
md += "### Code Health & Complexity\n"
|
| 439 |
md += "The AI flagged the following files as **Non-Standard** or **Complex** (Low Confidence).\n"
|
| 440 |
-
md +=
|
| 441 |
-
"These are good candidates for refactoring or documentation reviews:\n"
|
| 442 |
-
)
|
| 443 |
for f, score in top_refactors:
|
| 444 |
md += f"- `{f}` (Confidence: {int(score * 100)}%)\n"
|
| 445 |
md += "\n"
|
| 446 |
|
| 447 |
if top_couplings:
|
| 448 |
md += "### Logical Couplings\n"
|
| 449 |
-
md += "The AI detected strong semantic connections between these file pairs
|
| 450 |
for p1, p2, score in top_couplings:
|
| 451 |
md += f"- `{p1}` <--> `{p2}` ({int(score * 100)}% match)\n"
|
| 452 |
md += "\n"
|
|
@@ -463,283 +340,164 @@ class GuideGenerator:
|
|
| 463 |
|
| 464 |
md += "## Key Features\n"
|
| 465 |
if features:
|
| 466 |
-
md += "The following capabilities were inferred from the
|
| 467 |
for feature, description in features.items():
|
| 468 |
md += f"- **{feature}**: {description}\n"
|
| 469 |
else:
|
| 470 |
-
md += "No specific high-level features (Auth,
|
| 471 |
md += "\n"
|
| 472 |
|
| 473 |
md += "## Architecture & Technologies\n"
|
| 474 |
-
md += "The project utilizes the following core
|
| 475 |
-
|
| 476 |
if stack["languages"]:
|
| 477 |
md += "**Languages**: " + ", ".join(sorted(stack["languages"])) + "\n"
|
| 478 |
if stack["frameworks"]:
|
| 479 |
md += "**Frameworks**: " + ", ".join(sorted(stack["frameworks"])) + "\n"
|
| 480 |
if stack["tools"]:
|
| 481 |
-
md += "**Infrastructure**: " + ", ".join(sorted(stack["tools"])) + "\n"
|
| 482 |
if dev_tools:
|
| 483 |
md += "**Development Tools**: " + ", ".join(sorted(dev_tools)) + "\n"
|
| 484 |
md += "\n"
|
| 485 |
|
| 486 |
md += "## Getting Started\n\n"
|
| 487 |
-
|
| 488 |
-
# Config Section
|
| 489 |
-
if any(f.endswith(".env") or f.endswith(".env.example") for f in files):
|
| 490 |
-
md += "### Configuration\n"
|
| 491 |
-
md += "This project relies on environment variables. \n"
|
| 492 |
-
md += "1. Find the `.env.example` file in the root directory.\n"
|
| 493 |
-
md += "2. Copy it to create a new `.env` file.\n"
|
| 494 |
-
md += " `cp .env.example .env`\n"
|
| 495 |
-
md += "3. Fill in the required values (Database URL, API Keys, etc.).\n\n"
|
| 496 |
-
|
| 497 |
md += "### Prerequisites\n"
|
| 498 |
-
md += "Ensure you have the following installed:\n"
|
| 499 |
md += "- Git\n"
|
| 500 |
-
if
|
|
|
|
|
|
|
|
|
|
|
|
|
| 501 |
md += "- Node.js (LTS)\n"
|
| 502 |
-
if "Python" in stack["languages"]:
|
| 503 |
-
md += "- Python 3.8+\n"
|
| 504 |
-
if "Docker" in stack["tools"]:
|
| 505 |
-
md += "- Docker Desktop\n"
|
| 506 |
|
| 507 |
-
md += "\n### Installation\n"
|
| 508 |
-
md += "1. Clone
|
| 509 |
md += " ```bash\n"
|
| 510 |
-
md += f" git clone https://github.com/OWNER/{repo_name}.git\n"
|
| 511 |
md += f" cd {repo_name}\n"
|
| 512 |
md += " ```\n\n"
|
| 513 |
-
|
| 514 |
-
md += f"2. Install project dependencies:\n"
|
| 515 |
md += " ```bash\n"
|
| 516 |
md += f" {install_cmd}\n"
|
| 517 |
md += " ```\n\n"
|
| 518 |
-
|
| 519 |
-
md += "
|
| 520 |
-
md += "
|
| 521 |
-
md += "```
|
| 522 |
-
md += f"{run_cmd}\n"
|
| 523 |
-
md += "```\n\n"
|
| 524 |
|
| 525 |
if stats["Testing"] > 0:
|
| 526 |
md += "## Testing\n"
|
| 527 |
-
md += "Automated
|
| 528 |
md += f"```bash\n{test_cmd}\n```\n\n"
|
| 529 |
|
| 530 |
md += "## Project Structure\n"
|
| 531 |
-
md += "
|
| 532 |
md += "```text\n"
|
| 533 |
md += self._generate_tree_with_ai(files, layer_map)
|
| 534 |
md += "\n```\n\n"
|
| 535 |
|
| 536 |
-
# ... (rest of the markdown)
|
| 537 |
md += "## Contribution Workflow\n\n"
|
| 538 |
-
md += "
|
| 539 |
-
md += "
|
| 540 |
-
md += "
|
| 541 |
-
md += "
|
| 542 |
-
md += (
|
| 543 |
-
|
| 544 |
-
)
|
| 545 |
-
md += "### 2. Branching Strategy\n"
|
| 546 |
-
md += "Create a new branch from `main` using one of the following prefixes:\n"
|
| 547 |
-
md += "- `feat/`: For new features (e.g., `feat/user-auth`)\n"
|
| 548 |
-
md += "- `fix/`: For bug fixes (e.g., `fix/login-error`)\n"
|
| 549 |
-
md += "- `docs/`: For documentation changes (e.g., `docs/update-readme`)\n"
|
| 550 |
-
md += "- `refactor/`: For code improvements without logic changes\n"
|
| 551 |
-
md += "```bash\n"
|
| 552 |
-
md += "git checkout -b feat/your-feature-name\n"
|
| 553 |
-
md += "```\n\n"
|
| 554 |
-
md += "### 3. Development Standards\n"
|
| 555 |
-
if dev_tools:
|
| 556 |
-
md += "Before committing, ensure your code meets the project standards:\n"
|
| 557 |
-
if "ESLint" in dev_tools or "Prettier" in dev_tools:
|
| 558 |
-
md += "- **Linting**: Run `npm run lint` to fix style issues.\n"
|
| 559 |
-
if "Jest" in dev_tools or "pytest" in dev_tools:
|
| 560 |
-
md += "- **Testing**: Run tests to ensure no regressions.\n"
|
| 561 |
-
md += "- Keep pull requests small and focused on a single task.\n\n"
|
| 562 |
-
md += "### 4. Commit Messages\n"
|
| 563 |
-
md += "We follow the **Conventional Commits** specification:\n"
|
| 564 |
-
md += "- `feat: add new search component`\n"
|
| 565 |
-
md += "- `fix: handle null pointer exception`\n"
|
| 566 |
-
md += "- `chore: update dependencies`\n"
|
| 567 |
-
md += "```bash\n"
|
| 568 |
-
md += "git commit -m 'feat: implement amazing feature'\n"
|
| 569 |
-
md += "```\n\n"
|
| 570 |
-
md += "### 5. Pull Request Process\n"
|
| 571 |
-
md += "1. Push your branch: `git push origin feat/your-feature-name`\n"
|
| 572 |
-
md += "2. Open a Pull Request against the `main` branch.\n"
|
| 573 |
-
md += "3. Fill out the PR template with details about your changes.\n"
|
| 574 |
-
md += "4. Wait for code review and address any feedback.\n"
|
| 575 |
-
md += "5. Once approved, your changes will be merged!\n\n"
|
| 576 |
|
| 577 |
md += "## About this Guide\n"
|
| 578 |
-
md += "This
|
| 579 |
-
md += "- **Engine:** CodeBERT (Transformer-based Language Model)\n"
|
| 580 |
-
md += "- **Analysis:** Zero-shot semantic classification of file paths and content.\n"
|
| 581 |
-
md += "- **Accuracy:** The 'Project DNA' and 'Layer Composition' metrics are derived from vector embeddings of your codebase, providing a mathematical approximation of your architecture.\n"
|
| 582 |
|
| 583 |
return md
|
| 584 |
|
| 585 |
def _get_project_dna(self, stats: dict, total: int) -> str:
|
| 586 |
-
"""
|
| 587 |
-
Interprets the layer statistics to give a high-level project description.
|
| 588 |
-
"""
|
| 589 |
backend_pct = (stats["Backend"] / total) * 100
|
| 590 |
frontend_pct = (stats["Frontend"] / total) * 100
|
| 591 |
ops_pct = (stats["DevOps"] / total) * 100
|
| 592 |
|
| 593 |
if backend_pct > 50:
|
| 594 |
-
return "This project is a **Backend-focused
|
| 595 |
elif frontend_pct > 50:
|
| 596 |
-
return "This project is a **Frontend
|
| 597 |
elif backend_pct > 30 and frontend_pct > 30:
|
| 598 |
-
return "This is a balanced **Full-Stack
|
| 599 |
elif ops_pct > 40:
|
| 600 |
-
return "This repository
|
| 601 |
else:
|
| 602 |
-
return "This is a **General
|
| 603 |
|
| 604 |
def _get_testing_status(self, stats: dict, total: int) -> str:
|
| 605 |
test_pct = (stats["Testing"] / total) * 100
|
| 606 |
if test_pct > 20:
|
| 607 |
-
return "[Excellent]
|
| 608 |
elif test_pct > 5:
|
| 609 |
-
return "[Moderate]
|
| 610 |
else:
|
| 611 |
-
return "[Low]
|
| 612 |
|
| 613 |
def _detect_features(self, files: list[str], stats: dict) -> dict:
|
| 614 |
features = {}
|
| 615 |
files_str = " ".join(files).lower()
|
| 616 |
-
|
| 617 |
-
|
| 618 |
-
if stats["
|
| 619 |
-
|
| 620 |
-
|
| 621 |
-
|
| 622 |
-
|
| 623 |
-
|
| 624 |
-
"jwt",
|
| 625 |
-
"session",
|
| 626 |
-
"bcrypt",
|
| 627 |
-
"strategy",
|
| 628 |
-
]
|
| 629 |
-
if any(x in files_str for x in auth_indicators):
|
| 630 |
-
features["Authentication"] = (
|
| 631 |
-
"Implements user authentication (Login/Signup/Session management)."
|
| 632 |
-
)
|
| 633 |
-
|
| 634 |
-
# Database - Only if CodeBERT detected Backend logic
|
| 635 |
-
if stats["Backend"] > 0:
|
| 636 |
-
db_indicators = [
|
| 637 |
-
"schema.prisma",
|
| 638 |
-
"models.py",
|
| 639 |
-
"migration",
|
| 640 |
-
"sequelize",
|
| 641 |
-
"typeorm",
|
| 642 |
-
"mongoose",
|
| 643 |
-
"/db/",
|
| 644 |
-
"/database/",
|
| 645 |
-
]
|
| 646 |
-
if any(x in files_str for x in db_indicators):
|
| 647 |
-
features["Database"] = (
|
| 648 |
-
"Includes database schema definitions or ORM models."
|
| 649 |
-
)
|
| 650 |
-
|
| 651 |
-
# API
|
| 652 |
-
api_indicators = [
|
| 653 |
-
"/api/",
|
| 654 |
-
"controllers",
|
| 655 |
-
"resolvers",
|
| 656 |
-
"routes",
|
| 657 |
-
"router",
|
| 658 |
-
"endpoint",
|
| 659 |
-
]
|
| 660 |
-
if any(x in files_str for x in api_indicators):
|
| 661 |
-
features["API"] = "Exposes RESTful endpoints or GraphQL resolvers."
|
| 662 |
-
|
| 663 |
-
# Realtime
|
| 664 |
-
if any(x in files_str for x in ["socket", "websocket", "io.", "channel"]):
|
| 665 |
-
features["Real-time"] = "Uses WebSockets or real-time event channels."
|
| 666 |
-
|
| 667 |
-
# UI Architecture
|
| 668 |
-
if stats["Frontend"] > 0:
|
| 669 |
-
ui_indicators = ["components/", "views/", ".tsx", ".jsx", ".vue", "/pages/"]
|
| 670 |
-
if any(x in files_str for x in ui_indicators):
|
| 671 |
-
features["UI Architecture"] = "Modular component-based user interface."
|
| 672 |
-
|
| 673 |
return features
|
| 674 |
|
| 675 |
def _detect_dev_tools(self, files: list[str]) -> set:
|
| 676 |
tools = set()
|
| 677 |
files_str = " ".join(files).lower()
|
| 678 |
-
|
| 679 |
if "eslint" in files_str:
|
| 680 |
tools.add("ESLint")
|
| 681 |
if "prettier" in files_str:
|
| 682 |
tools.add("Prettier")
|
| 683 |
if "jest" in files_str:
|
| 684 |
tools.add("Jest")
|
| 685 |
-
if "cypress" in files_str:
|
| 686 |
-
tools.add("Cypress")
|
| 687 |
if "github/workflows" in files_str:
|
| 688 |
tools.add("GitHub Actions")
|
| 689 |
-
if "husky" in files_str:
|
| 690 |
-
tools.add("Husky")
|
| 691 |
if "tailwind" in files_str:
|
| 692 |
tools.add("Tailwind CSS")
|
| 693 |
-
if "
|
| 694 |
-
tools.add("
|
| 695 |
-
if "webpack" in files_str:
|
| 696 |
-
tools.add("Webpack")
|
| 697 |
-
|
| 698 |
return tools
|
| 699 |
|
| 700 |
def _generate_tree_with_ai(self, files: list[str], layer_map: dict) -> str:
|
| 701 |
tree = {}
|
| 702 |
for f in files:
|
| 703 |
parts = f.split("/")
|
| 704 |
-
if any(p in ["node_modules", ".git", "__pycache__", "dist"] for p in parts):
|
| 705 |
continue
|
| 706 |
-
|
| 707 |
curr = tree
|
| 708 |
-
for part in parts[:3]:
|
| 709 |
curr = curr.setdefault(part, {})
|
| 710 |
|
| 711 |
lines = []
|
| 712 |
-
|
| 713 |
def render(node, path_prefix="", tree_prefix=""):
|
| 714 |
keys = sorted(node.keys())
|
| 715 |
for i, key in enumerate(keys):
|
| 716 |
is_last = i == len(keys) - 1
|
| 717 |
full_path = f"{path_prefix}/{key}".strip("/")
|
| 718 |
-
|
| 719 |
-
# Get layer from map or predict if it's a folder
|
| 720 |
prediction = classifier.predict(full_path)
|
| 721 |
layer = layer_map.get(full_path, prediction["label"])
|
| 722 |
label = f" [{layer}]" if layer != "Generic" else ""
|
| 723 |
-
|
| 724 |
connector = "└── " if is_last else "├── "
|
| 725 |
lines.append(f"{tree_prefix}{connector}{key}{label}")
|
| 726 |
-
|
| 727 |
if node[key]:
|
| 728 |
-
render(
|
| 729 |
-
node[key],
|
| 730 |
-
full_path,
|
| 731 |
-
tree_prefix + (" " if is_last else "│ "),
|
| 732 |
-
)
|
| 733 |
|
| 734 |
render(tree)
|
| 735 |
return "\n".join(lines[:60])
|
| 736 |
|
| 737 |
|
| 738 |
-
#
|
| 739 |
classifier = CodeClassifier()
|
| 740 |
guide_generator = GuideGenerator()
|
| 741 |
|
| 742 |
class ReviewWrapper:
|
| 743 |
def __init__(self):
|
| 744 |
self.service = AIReviewerService()
|
|
|
|
| 745 |
reviewer = ReviewWrapper()
|
|
|
|
|
|
|
|
|
|
| 1 |
import logging
|
| 2 |
+
import re
|
| 3 |
import torch
|
| 4 |
import torch.nn.functional as F
|
| 5 |
from transformers import AutoModel, AutoTokenizer
|
|
|
|
| 6 |
|
| 7 |
+
# Resilient import handling for AIReviewerService across different project layouts
|
| 8 |
+
try:
|
| 9 |
+
from app.services.reviewer_service import AIReviewerService
|
| 10 |
+
except ImportError:
|
| 11 |
+
try:
|
| 12 |
+
from app.predictor.reviewer import AIReviewerService
|
| 13 |
+
except ImportError:
|
| 14 |
+
from app.reviewer import AIReviewerService
|
| 15 |
+
|
| 16 |
logging.basicConfig(level=logging.INFO)
|
| 17 |
logger = logging.getLogger(__name__)
|
| 18 |
|
| 19 |
|
| 20 |
class CodeClassifier:
|
| 21 |
+
"""
|
| 22 |
+
CodeBERT-based classifier for source code architecture layer prediction,
|
| 23 |
+
semantic embedding generation, file summarization, and tag extraction.
|
| 24 |
+
"""
|
| 25 |
def __init__(self):
|
| 26 |
+
logger.info("⏳ Initializing CodeBERT AI Service...")
|
| 27 |
|
|
|
|
| 28 |
self.device = "cuda" if torch.cuda.is_available() else "cpu"
|
| 29 |
if torch.backends.mps.is_available():
|
| 30 |
self.device = "mps"
|
| 31 |
|
| 32 |
logger.info(f"🚀 Running on device: {self.device}")
|
| 33 |
|
|
|
|
|
|
|
| 34 |
try:
|
| 35 |
+
logger.info("📥 Loading microsoft/codebert-base Model...")
|
|
|
|
|
|
|
| 36 |
self.tokenizer = AutoTokenizer.from_pretrained("microsoft/codebert-base")
|
| 37 |
self.model = AutoModel.from_pretrained("microsoft/codebert-base").to(
|
| 38 |
self.device
|
| 39 |
)
|
| 40 |
+
logger.info("✅ CodeBERT Model Loaded Successfully!")
|
| 41 |
except Exception as e:
|
| 42 |
+
logger.error(f"❌ Failed to load CodeBERT model: {e}")
|
| 43 |
raise e
|
| 44 |
|
|
|
|
| 45 |
self.labels = {
|
| 46 |
+
"Frontend": "import react component from view styles css html dom window document state props effect ui compose jetpack layout",
|
| 47 |
+
"Backend": "import express nest controller service entity repository database sql mongoose route api async await req res dto spring ktor",
|
| 48 |
+
"Security": "import auth passport jwt strategy bcrypt verify token secret guard password user login session middleware rls permissions",
|
| 49 |
+
"DevOps": "docker build image container kubernetes yaml env port host volume deploy pipeline stage steps runs-on actions workflow",
|
| 50 |
+
"Testing": "describe it expect test mock spy jest beforeall aftereach suite spec assert testcase runner junit mockk",
|
| 51 |
}
|
| 52 |
self.label_embeddings = self._precompute_label_embeddings()
|
| 53 |
|
| 54 |
def _get_embedding(self, text: str):
|
| 55 |
+
"""Generates a 768-dim vector representation using CodeBERT."""
|
|
|
|
|
|
|
|
|
|
| 56 |
inputs = self.tokenizer(
|
| 57 |
text, return_tensors="pt", padding=True, truncation=True, max_length=512
|
| 58 |
).to(self.device)
|
| 59 |
with torch.no_grad():
|
| 60 |
outputs = self.model(**inputs)
|
|
|
|
| 61 |
return outputs.last_hidden_state[:, 0, :]
|
| 62 |
|
| 63 |
def _precompute_label_embeddings(self):
|
| 64 |
+
"""Precomputes vector representations for category anchors at startup."""
|
| 65 |
+
logger.info("🧠 Pre-computing semantic anchors for layer classification...")
|
|
|
|
|
|
|
| 66 |
embeddings = {}
|
| 67 |
for label, description in self.labels.items():
|
| 68 |
embeddings[label] = self._get_embedding(description)
|
| 69 |
return embeddings
|
| 70 |
|
|
|
|
| 71 |
def predict(self, file_path: str, content: str = None) -> dict:
|
| 72 |
"""
|
| 73 |
+
Determines the architectural layer of a file and outputs a flat 1D vector embedding.
|
| 74 |
+
Returns: { "label": str, "confidence": float, "embedding": List[float] }
|
| 75 |
"""
|
| 76 |
path = file_path.lower()
|
| 77 |
|
| 78 |
+
try:
|
| 79 |
+
text_to_analyze = content[:1000] if content else file_path
|
| 80 |
+
target_embedding_tensor = self._get_embedding(text_to_analyze)
|
| 81 |
+
target_embedding_list = target_embedding_tensor.squeeze(0).tolist()
|
| 82 |
+
except Exception as e:
|
| 83 |
+
logger.error(f"Embedding computation error for {file_path}: {e}")
|
| 84 |
+
target_embedding_tensor = None
|
| 85 |
+
target_embedding_list = []
|
| 86 |
+
|
| 87 |
+
def build_result(label, conf=1.0):
|
| 88 |
return {
|
| 89 |
"label": label,
|
| 90 |
"confidence": conf,
|
| 91 |
+
"embedding": target_embedding_list,
|
| 92 |
}
|
| 93 |
|
| 94 |
+
# 1. Fast Path: High Precision Rule Matching
|
| 95 |
+
if any(x in path for x in ["/components/", "/pages/", "/views/", "/ui/", ".jsx", ".tsx", ".css", "tailwind", "compose"]):
|
| 96 |
+
return build_result("Frontend")
|
| 97 |
+
if any(x in path for x in ["/controllers/", "/modules/", "/services/", "/repository/", ".controller.ts", ".service.ts", "dto", "ktor", "route"]):
|
| 98 |
+
return build_result("Backend")
|
| 99 |
+
if any(x in path for x in ["auth", "guard", "strategy", "jwt", "passport", "middleware", "security"]):
|
| 100 |
+
return build_result("Security")
|
| 101 |
+
if any(x in path for x in ["docker", "k8s", "github/workflows", "tsconfig", "package.json", "build.gradle", "pom.xml"]):
|
| 102 |
+
return build_result("DevOps")
|
| 103 |
+
if any(x in path for x in ["test", "spec", "e2e", "jest", "mockk", "androidTest"]):
|
| 104 |
+
return build_result("Testing")
|
| 105 |
+
|
| 106 |
+
# 2. Slow Path: AI Semantic Distance Fallback
|
| 107 |
+
if target_embedding_tensor is None:
|
| 108 |
+
return build_result("Generic", 0.0)
|
| 109 |
+
|
| 110 |
+
best_label = "Generic"
|
| 111 |
+
highest_score = -1.0
|
| 112 |
+
|
| 113 |
+
for label, anchor_embedding in self.label_embeddings.items():
|
| 114 |
+
score = F.cosine_similarity(target_embedding_tensor, anchor_embedding).item()
|
| 115 |
+
if score > highest_score:
|
| 116 |
+
highest_score = score
|
| 117 |
+
best_label = label
|
| 118 |
+
|
| 119 |
+
if highest_score > 0.25:
|
| 120 |
+
return build_result(best_label, float(highest_score))
|
| 121 |
+
|
| 122 |
+
return build_result("Generic", float(highest_score))
|
| 123 |
+
|
| 124 |
+
def semantic_search(self, query: str, embeddings_map: dict) -> list:
|
| 125 |
+
"""Executes vector similarity search against cached repository embeddings."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 126 |
try:
|
| 127 |
+
query_emb = self._get_embedding(query).cpu()
|
| 128 |
+
results = []
|
| 129 |
+
for file_path, emb_list in embeddings_map.items():
|
| 130 |
+
if not emb_list:
|
| 131 |
+
continue
|
| 132 |
+
file_emb = torch.tensor(emb_list).view(1, -1).cpu()
|
| 133 |
+
score = F.cosine_similarity(query_emb, file_emb).item()
|
| 134 |
+
results.append({"fileName": file_path, "score": round(float(score), 4)})
|
| 135 |
+
results.sort(key=lambda x: x["score"], reverse=True)
|
| 136 |
+
return results[:10]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 137 |
except Exception as e:
|
| 138 |
+
logger.error(f"Semantic search failed: {e}")
|
| 139 |
+
return []
|
| 140 |
+
|
| 141 |
+
def generate_file_summary(self, content: str = None, file_name: str = "") -> str:
|
| 142 |
+
"""Generates a concise summary description for a single source file."""
|
| 143 |
+
if not content:
|
| 144 |
+
return f"Source file: {file_name}"
|
| 145 |
+
lines = [l.strip() for l in content.split("\n") if l.strip()]
|
| 146 |
+
non_comment_lines = [l for l in lines if not l.startswith(("//", "#", "/*", "*"))]
|
| 147 |
+
return f"File '{file_name}' containing {len(lines)} total lines ({len(non_comment_lines)} logic lines)."
|
| 148 |
+
|
| 149 |
+
def extract_tags(self, content: str = None, file_name: str = "") -> list:
|
| 150 |
+
"""Extracts contextual tags from file extensions and code syntax."""
|
| 151 |
+
tags = set()
|
| 152 |
+
ext = file_name.rsplit(".", 1)[-1].lower() if "." in file_name else ""
|
| 153 |
+
if ext:
|
| 154 |
+
tags.add(ext)
|
| 155 |
+
|
| 156 |
+
if content:
|
| 157 |
+
c = content.lower()
|
| 158 |
+
if "import " in c or "require(" in c:
|
| 159 |
+
tags.add("dependencies")
|
| 160 |
+
if "async " in c or "coroutine" in c or "promise" in c or "suspend " in c:
|
| 161 |
+
tags.add("async")
|
| 162 |
+
if "class " in c or "interface " in c:
|
| 163 |
+
tags.add("object-oriented")
|
| 164 |
+
if "function" in c or "fun " in c or "def " in c:
|
| 165 |
+
tags.add("functional")
|
| 166 |
+
if "stateflow" in c or "livedata" in c or "usestate" in c:
|
| 167 |
+
tags.add("state-management")
|
| 168 |
+
return list(tags)
|
| 169 |
|
| 170 |
|
| 171 |
class GuideGenerator:
|
| 172 |
+
"""
|
| 173 |
+
Generates developer documentation, architectural tree structures, and project summaries.
|
| 174 |
+
"""
|
| 175 |
def __init__(self):
|
| 176 |
self.tech_stacks = {
|
| 177 |
"React": ["react", "jsx", "tsx", "next.config.js"],
|
|
|
|
| 184 |
"Django": ["django", "manage.py"],
|
| 185 |
"Flask": ["flask", "app.py"],
|
| 186 |
"Spring Boot": ["pom.xml", "build.gradle", "src/main/java"],
|
| 187 |
+
"Android (Jetpack Compose)": ["build.gradle.kts", "compose", "activity_main.xml", "androidmanifest.xml"],
|
| 188 |
"Go": ["go.mod", "main.go"],
|
| 189 |
"Rust": ["Cargo.toml", "src/main.rs"],
|
| 190 |
}
|
|
|
|
| 191 |
self.tools = {
|
| 192 |
"Docker": ["Dockerfile", "docker-compose.yml"],
|
| 193 |
"Kubernetes": ["k8s", "helm", "charts/"],
|
|
|
|
| 198 |
"PostgreSQL": ["postgresql", "pg"],
|
| 199 |
"MongoDB": ["mongoose", "mongodb"],
|
| 200 |
"Redis": ["redis"],
|
| 201 |
+
"Supabase / Firebase": ["supabase", "firebase", "firestore"],
|
| 202 |
+
"Ktor / Retrofit": ["ktor", "retrofit", "okhttp"],
|
| 203 |
+
"Dagger Hilt / Koin": ["hilt", "koin", "dagger"],
|
| 204 |
}
|
| 205 |
|
| 206 |
def detect_stack(self, files: list[str]) -> dict:
|
| 207 |
detected = {"languages": set(), "frameworks": set(), "tools": set()}
|
|
|
|
| 208 |
for file in files:
|
| 209 |
path = file.lower()
|
| 210 |
|
|
|
|
| 221 |
detected["languages"].add("Rust")
|
| 222 |
elif path.endswith(".java"):
|
| 223 |
detected["languages"].add("Java")
|
| 224 |
+
elif path.endswith(".kt") or path.endswith(".kts"):
|
| 225 |
+
detected["languages"].add("Kotlin")
|
| 226 |
|
| 227 |
# Frameworks
|
| 228 |
for framework, indicators in self.tech_stacks.items():
|
| 229 |
if any(ind in path for ind in indicators):
|
| 230 |
detected["frameworks"].add(framework)
|
| 231 |
|
| 232 |
+
# Tools & Libraries
|
| 233 |
for tool, indicators in self.tools.items():
|
| 234 |
if any(ind in path for ind in indicators):
|
| 235 |
detected["tools"].add(tool)
|
| 236 |
|
| 237 |
return detected
|
| 238 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 239 |
def generate_markdown(self, repo_name: str, files: list[str]) -> str:
|
| 240 |
+
"""Generates a comprehensive developer guide formatted in Markdown."""
|
| 241 |
+
stats = {"Frontend": 0, "Backend": 0, "Security": 0, "DevOps": 0, "Testing": 0, "Generic": 0}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 242 |
layer_map = {}
|
|
|
|
| 243 |
low_confidence_files = []
|
| 244 |
+
file_embeddings = {}
|
| 245 |
|
| 246 |
for f in files:
|
|
|
|
| 247 |
prediction = classifier.predict(f)
|
| 248 |
layer = prediction["label"]
|
| 249 |
confidence = prediction["confidence"]
|
|
|
|
| 254 |
if confidence < 0.4 and layer != "Generic":
|
| 255 |
low_confidence_files.append((f, confidence))
|
| 256 |
|
| 257 |
+
if prediction["embedding"]:
|
| 258 |
+
file_embeddings[f] = torch.tensor(prediction["embedding"]).view(1, -1)
|
|
|
|
| 259 |
|
| 260 |
total_files = len(files) if files else 1
|
| 261 |
primary_layer = max(stats, key=stats.get)
|
| 262 |
|
| 263 |
+
# Cross-file logical couplings analysis
|
| 264 |
couplings = []
|
| 265 |
try:
|
| 266 |
+
sample_paths = list(file_embeddings.keys())[:50]
|
|
|
|
|
|
|
|
|
|
| 267 |
for i in range(len(sample_paths)):
|
| 268 |
for j in range(i + 1, len(sample_paths)):
|
| 269 |
p1, p2 = sample_paths[i], sample_paths[j]
|
|
|
|
| 270 |
if p1.rsplit("/", 1)[0] == p2.rsplit("/", 1)[0]:
|
| 271 |
continue
|
| 272 |
|
|
|
|
| 273 |
t1 = file_embeddings[p1].cpu()
|
| 274 |
t2 = file_embeddings[p2].cpu()
|
| 275 |
|
| 276 |
+
score = F.cosine_similarity(t1, t2).item()
|
| 277 |
if score > 0.88:
|
| 278 |
couplings.append((p1, p2, score))
|
| 279 |
except Exception as e:
|
|
|
|
| 282 |
couplings.sort(key=lambda x: x[2], reverse=True)
|
| 283 |
top_couplings = couplings[:5]
|
| 284 |
|
|
|
|
| 285 |
low_confidence_files.sort(key=lambda x: x[1])
|
| 286 |
top_refactors = low_confidence_files[:5]
|
| 287 |
|
|
|
|
| 288 |
stack = self.detect_stack(files)
|
| 289 |
features = self._detect_features(files, stats)
|
| 290 |
dev_tools = self._detect_dev_tools(files)
|
|
|
|
| 293 |
run_cmd = "npm run dev"
|
| 294 |
test_cmd = "npm test"
|
| 295 |
|
| 296 |
+
if "Kotlin" in stack["languages"] or "Java" in stack["languages"]:
|
| 297 |
+
install_cmd = "./gradlew build"
|
| 298 |
+
run_cmd = "./gradlew assembleDebug"
|
| 299 |
+
test_cmd = "./gradlew test"
|
| 300 |
+
elif "Python" in stack["languages"]:
|
| 301 |
install_cmd = "pip install -r requirements.txt"
|
| 302 |
run_cmd = "python main.py"
|
| 303 |
test_cmd = "pytest"
|
|
|
|
| 305 |
install_cmd = "go mod download"
|
| 306 |
run_cmd = "go run main.go"
|
| 307 |
test_cmd = "go test ./..."
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 308 |
|
| 309 |
+
# Assemble Markdown Document
|
| 310 |
+
md = f"# {repo_name} Developer Guide\n\n"
|
| 311 |
md += "## AI Codebase Insights\n"
|
| 312 |
+
md += "Analysis powered by **CodeBERT** semantic vector embeddings.\n\n"
|
| 313 |
md += f"**Project DNA:** {self._get_project_dna(stats, total_files)}\n\n"
|
| 314 |
md += f"**Quality Check:** {self._get_testing_status(stats, total_files)}\n\n"
|
| 315 |
|
| 316 |
if top_refactors:
|
| 317 |
md += "### Code Health & Complexity\n"
|
| 318 |
md += "The AI flagged the following files as **Non-Standard** or **Complex** (Low Confidence).\n"
|
| 319 |
+
md += "These are recommended candidates for refactoring or architectural review:\n"
|
|
|
|
|
|
|
| 320 |
for f, score in top_refactors:
|
| 321 |
md += f"- `{f}` (Confidence: {int(score * 100)}%)\n"
|
| 322 |
md += "\n"
|
| 323 |
|
| 324 |
if top_couplings:
|
| 325 |
md += "### Logical Couplings\n"
|
| 326 |
+
md += "The AI detected strong semantic connections between these file pairs across different directories:\n"
|
| 327 |
for p1, p2, score in top_couplings:
|
| 328 |
md += f"- `{p1}` <--> `{p2}` ({int(score * 100)}% match)\n"
|
| 329 |
md += "\n"
|
|
|
|
| 340 |
|
| 341 |
md += "## Key Features\n"
|
| 342 |
if features:
|
| 343 |
+
md += "The following capabilities were inferred from the repository structure:\n\n"
|
| 344 |
for feature, description in features.items():
|
| 345 |
md += f"- **{feature}**: {description}\n"
|
| 346 |
else:
|
| 347 |
+
md += "No specific high-level features (Auth, DB, etc.) were explicitly identified.\n"
|
| 348 |
md += "\n"
|
| 349 |
|
| 350 |
md += "## Architecture & Technologies\n"
|
| 351 |
+
md += "The project utilizes the following core technology stack:\n\n"
|
|
|
|
| 352 |
if stack["languages"]:
|
| 353 |
md += "**Languages**: " + ", ".join(sorted(stack["languages"])) + "\n"
|
| 354 |
if stack["frameworks"]:
|
| 355 |
md += "**Frameworks**: " + ", ".join(sorted(stack["frameworks"])) + "\n"
|
| 356 |
if stack["tools"]:
|
| 357 |
+
md += "**Infrastructure & Libraries**: " + ", ".join(sorted(stack["tools"])) + "\n"
|
| 358 |
if dev_tools:
|
| 359 |
md += "**Development Tools**: " + ", ".join(sorted(dev_tools)) + "\n"
|
| 360 |
md += "\n"
|
| 361 |
|
| 362 |
md += "## Getting Started\n\n"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 363 |
md += "### Prerequisites\n"
|
| 364 |
+
md += "Ensure you have the following installed on your machine:\n"
|
| 365 |
md += "- Git\n"
|
| 366 |
+
if "Kotlin" in stack["languages"] or "Java" in stack["languages"]:
|
| 367 |
+
md += "- JDK 17+\n- Android Studio / IntelliJ IDEA\n"
|
| 368 |
+
elif "Python" in stack["languages"]:
|
| 369 |
+
md += "- Python 3.10+\n"
|
| 370 |
+
else:
|
| 371 |
md += "- Node.js (LTS)\n"
|
|
|
|
|
|
|
|
|
|
|
|
|
| 372 |
|
| 373 |
+
md += "\n### Installation & Setup\n"
|
| 374 |
+
md += "1. Clone the repository:\n"
|
| 375 |
md += " ```bash\n"
|
| 376 |
+
md += f" git clone [https://github.com/OWNER/](https://github.com/OWNER/){repo_name}.git\n"
|
| 377 |
md += f" cd {repo_name}\n"
|
| 378 |
md += " ```\n\n"
|
| 379 |
+
md += "2. Install dependencies:\n"
|
|
|
|
| 380 |
md += " ```bash\n"
|
| 381 |
md += f" {install_cmd}\n"
|
| 382 |
md += " ```\n\n"
|
| 383 |
+
md += "3. Run the application:\n"
|
| 384 |
+
md += " ```bash\n"
|
| 385 |
+
md += f" {run_cmd}\n"
|
| 386 |
+
md += " ```\n\n"
|
|
|
|
|
|
|
| 387 |
|
| 388 |
if stats["Testing"] > 0:
|
| 389 |
md += "## Testing\n"
|
| 390 |
+
md += "Automated test suites detected. Run them using:\n"
|
| 391 |
md += f"```bash\n{test_cmd}\n```\n\n"
|
| 392 |
|
| 393 |
md += "## Project Structure\n"
|
| 394 |
+
md += "Hierarchical tree layout annotated with AI layer predictions:\n\n"
|
| 395 |
md += "```text\n"
|
| 396 |
md += self._generate_tree_with_ai(files, layer_map)
|
| 397 |
md += "\n```\n\n"
|
| 398 |
|
|
|
|
| 399 |
md += "## Contribution Workflow\n\n"
|
| 400 |
+
md += "1. **Create a Feature Branch**:\n"
|
| 401 |
+
md += " ```bash\n"
|
| 402 |
+
md += " git checkout -b feat/your-feature-name\n"
|
| 403 |
+
md += " ```\n"
|
| 404 |
+
md += "2. **Commit Standards**: Follow Conventional Commits format (`feat:`, `fix:`, `refactor:`).\n"
|
| 405 |
+
md += "3. **Open Pull Request**: Submit your branch for code review against `main`.\n\n"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 406 |
|
| 407 |
md += "## About this Guide\n"
|
| 408 |
+
md += "This document was dynamically synthesized by **GitGud AI** using transformer-based CodeBERT embeddings.\n"
|
|
|
|
|
|
|
|
|
|
| 409 |
|
| 410 |
return md
|
| 411 |
|
| 412 |
def _get_project_dna(self, stats: dict, total: int) -> str:
|
|
|
|
|
|
|
|
|
|
| 413 |
backend_pct = (stats["Backend"] / total) * 100
|
| 414 |
frontend_pct = (stats["Frontend"] / total) * 100
|
| 415 |
ops_pct = (stats["DevOps"] / total) * 100
|
| 416 |
|
| 417 |
if backend_pct > 50:
|
| 418 |
+
return "This project is a **Backend-focused Service**, dedicated to business logic and data persistence."
|
| 419 |
elif frontend_pct > 50:
|
| 420 |
+
return "This project is a **Frontend / Mobile Application**, focusing on user interface and client experience."
|
| 421 |
elif backend_pct > 30 and frontend_pct > 30:
|
| 422 |
+
return "This is a balanced **Full-Stack / Multi-Module Project** with strong client and server logic."
|
| 423 |
elif ops_pct > 40:
|
| 424 |
+
return "This repository is an **Infrastructure or DevOps Configuration** project."
|
| 425 |
else:
|
| 426 |
+
return "This is a **General-Purpose Codebase** or modular utility repository."
|
| 427 |
|
| 428 |
def _get_testing_status(self, stats: dict, total: int) -> str:
|
| 429 |
test_pct = (stats["Testing"] / total) * 100
|
| 430 |
if test_pct > 20:
|
| 431 |
+
return "[Excellent] High automated test coverage detected across modules."
|
| 432 |
elif test_pct > 5:
|
| 433 |
+
return "[Moderate] Partial test coverage present."
|
| 434 |
else:
|
| 435 |
+
return "[Low] Low test coverage. Adding unit and integration tests is recommended."
|
| 436 |
|
| 437 |
def _detect_features(self, files: list[str], stats: dict) -> dict:
|
| 438 |
features = {}
|
| 439 |
files_str = " ".join(files).lower()
|
| 440 |
+
if stats["Security"] > 0 or any(x in files_str for x in ["auth", "login", "jwt", "passport"]):
|
| 441 |
+
features["Authentication"] = "Implements user authentication & session management."
|
| 442 |
+
if stats["Backend"] > 0 or any(x in files_str for x in ["db", "schema", "model", "room", "firestore", "supabase"]):
|
| 443 |
+
features["Database & Persistence"] = "Includes database ORM models, schemas, or persistent repositories."
|
| 444 |
+
if any(x in files_str for x in ["api", "controller", "retrofit", "ktor", "route"]):
|
| 445 |
+
features["API Integration"] = "Exposes REST/GraphQL endpoints or handles remote HTTP communication."
|
| 446 |
+
if any(x in files_str for x in ["viewmodel", "stateflow", "livedata", "redux", "usestate"]):
|
| 447 |
+
features["State Management"] = "Utilizes explicit reactive architectural state management."
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 448 |
return features
|
| 449 |
|
| 450 |
def _detect_dev_tools(self, files: list[str]) -> set:
|
| 451 |
tools = set()
|
| 452 |
files_str = " ".join(files).lower()
|
|
|
|
| 453 |
if "eslint" in files_str:
|
| 454 |
tools.add("ESLint")
|
| 455 |
if "prettier" in files_str:
|
| 456 |
tools.add("Prettier")
|
| 457 |
if "jest" in files_str:
|
| 458 |
tools.add("Jest")
|
|
|
|
|
|
|
| 459 |
if "github/workflows" in files_str:
|
| 460 |
tools.add("GitHub Actions")
|
|
|
|
|
|
|
| 461 |
if "tailwind" in files_str:
|
| 462 |
tools.add("Tailwind CSS")
|
| 463 |
+
if "gradle" in files_str:
|
| 464 |
+
tools.add("Gradle Build Tool")
|
|
|
|
|
|
|
|
|
|
| 465 |
return tools
|
| 466 |
|
| 467 |
def _generate_tree_with_ai(self, files: list[str], layer_map: dict) -> str:
|
| 468 |
tree = {}
|
| 469 |
for f in files:
|
| 470 |
parts = f.split("/")
|
| 471 |
+
if any(p in ["node_modules", ".git", "__pycache__", "dist", "build", ".idea"] for p in parts):
|
| 472 |
continue
|
|
|
|
| 473 |
curr = tree
|
| 474 |
+
for part in parts[:3]:
|
| 475 |
curr = curr.setdefault(part, {})
|
| 476 |
|
| 477 |
lines = []
|
|
|
|
| 478 |
def render(node, path_prefix="", tree_prefix=""):
|
| 479 |
keys = sorted(node.keys())
|
| 480 |
for i, key in enumerate(keys):
|
| 481 |
is_last = i == len(keys) - 1
|
| 482 |
full_path = f"{path_prefix}/{key}".strip("/")
|
|
|
|
|
|
|
| 483 |
prediction = classifier.predict(full_path)
|
| 484 |
layer = layer_map.get(full_path, prediction["label"])
|
| 485 |
label = f" [{layer}]" if layer != "Generic" else ""
|
|
|
|
| 486 |
connector = "└── " if is_last else "├── "
|
| 487 |
lines.append(f"{tree_prefix}{connector}{key}{label}")
|
|
|
|
| 488 |
if node[key]:
|
| 489 |
+
render(node[key], full_path, tree_prefix + (" " if is_last else "│ "))
|
|
|
|
|
|
|
|
|
|
|
|
|
| 490 |
|
| 491 |
render(tree)
|
| 492 |
return "\n".join(lines[:60])
|
| 493 |
|
| 494 |
|
| 495 |
+
# Global Singleton Instances
|
| 496 |
classifier = CodeClassifier()
|
| 497 |
guide_generator = GuideGenerator()
|
| 498 |
|
| 499 |
class ReviewWrapper:
|
| 500 |
def __init__(self):
|
| 501 |
self.service = AIReviewerService()
|
| 502 |
+
|
| 503 |
reviewer = ReviewWrapper()
|