CodeCommunity commited on
Commit
d948aa0
·
verified ·
1 Parent(s): 9ab4eb2

Update app/predictor.py

Browse files
Files changed (1) hide show
  1. 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
- # just use to log at terminal no big deal here
 
 
 
 
 
 
 
 
10
  logging.basicConfig(level=logging.INFO)
11
  logger = logging.getLogger(__name__)
12
 
13
 
14
  class CodeClassifier:
15
- # (initializing/downloading/caching) the codeBERT_model first time it will take some time as downloading (500_MB)
 
 
 
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
- Computes embeddings for the category descriptions once at startup.
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 'Layer' of a file (Frontend, Backend, etc.)
78
- Returns: { "label": str, "confidence": float, "embedding": list[float] }
79
  """
80
  path = file_path.lower()
81
 
82
- # Helper to standardize return format
83
- def result(label, conf=1.0, emb=None):
 
 
 
 
 
 
 
 
84
  return {
85
  "label": label,
86
  "confidence": conf,
87
- "embedding": emb if emb is not None else [],
88
  }
89
 
90
- # 1. Fast Path: Rule-Based Checks (High Precision)
91
- # We still keep this because it's instant and correct for obvious things.
92
-
93
- # Frontend Indicators
94
- if any(
95
- x in path
96
- for x in [
97
- "/components/",
98
- "/pages/",
99
- "/views/",
100
- ".jsx",
101
- ".tsx",
102
- ".css",
103
- "tailwind",
104
- ]
105
- ):
106
- return result("Frontend")
107
-
108
- # Backend Indicators
109
- if any(
110
- x in path
111
- for x in [
112
- "/controllers/",
113
- "/modules/",
114
- "/services/",
115
- ".controller.ts",
116
- ".service.ts",
117
- "dto",
118
- ]
119
- ):
120
- return result("Backend")
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
- # Decide what to analyze: Content (best) or Path (fallback)
144
- text_to_analyze = content if content else file_path
145
-
146
- # If analyzing content, take the first 1000 chars (approx enough tokens) to capture imports/class defs
147
- if content:
148
- text_to_analyze = content[:1000]
149
-
150
- target_embedding_tensor = self._get_embedding(text_to_analyze)
151
- target_embedding_list = (
152
- target_embedding_tensor.tolist()
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"AI Classification failed for {file_path}: {e}")
175
- return result("Generic", 0.0)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- # 1. Perform AI Analysis (DNA of the repo)
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 = {} # Path -> Tensor
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
- # Store embedding for coupling analysis (if available)
361
- if prediction["embedding"] and len(prediction["embedding"]) > 0:
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
- # Calculate Semantic Couplings (Top 5)
368
  couplings = []
369
  try:
370
- paths = list(file_embeddings.keys())
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.unsqueeze(0), t2.unsqueeze(0)).item()
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 "Python" in stack["languages"]:
 
 
 
 
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 += f"Analysis powered by **CodeBERT** semantic vectors.\n\n"
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 (they share logic but not folders):\n"
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 codebase structure:\n\n"
467
  for feature, description in features.items():
468
  md += f"- **{feature}**: {description}\n"
469
  else:
470
- md += "No specific high-level features (Auth, Database, etc.) were explicitly detected from the file structure.\n"
471
  md += "\n"
472
 
473
  md += "## Architecture & Technologies\n"
474
- md += "The project utilizes the following core technologies:\n\n"
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 any(x in ["TypeScript", "JavaScript"] for x in stack["languages"]):
 
 
 
 
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 and enter the repository:\n"
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 += "### Execution\n"
520
- md += "Start the development environment:\n"
521
- md += "```bash\n"
522
- md += f"{run_cmd}\n"
523
- md += "```\n\n"
524
 
525
  if stats["Testing"] > 0:
526
  md += "## Testing\n"
527
- md += "Automated tests were detected. Run them using:\n"
528
  md += f"```bash\n{test_cmd}\n```\n\n"
529
 
530
  md += "## Project Structure\n"
531
- md += "Detailed tree view with AI-predicted layer labels:\n\n"
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 += "We welcome contributions! Please follow this detailed workflow to ensure smooth collaboration:\n\n"
539
- md += "### 1. Find an Issue\n"
540
- md += "- Browse the **Issues** tab for tasks.\n"
541
- md += "- Look for labels like `good first issue` or `help wanted` if you are new.\n"
542
- md += (
543
- "- Comment on the issue to assign it to yourself before starting work.\n\n"
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 documentation was automatically generated by **GitGud AI**.\n"
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 service**, likely an API or microservice. The majority of the codebase is dedicated to business logic and data handling."
595
  elif frontend_pct > 50:
596
- return "This project is a **Frontend-heavy application**, focusing on UI/UX and client-side logic."
597
  elif backend_pct > 30 and frontend_pct > 30:
598
- return "This is a balanced **Full-Stack Application**, containing significant logic for both client and server components."
599
  elif ops_pct > 40:
600
- return "This repository appears to be an **Infrastructure or Configuration** project (IaC), heavily focused on deployment and orchestration."
601
  else:
602
- return "This is a **General purpose repository**, possibly a library or a mix of various utilities."
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] **Excellent Test Coverage**. A significant portion of the codebase is dedicated to testing."
608
  elif test_pct > 5:
609
- return "[Moderate] **Moderate Testing**. Tests are present but may not cover all modules."
610
  else:
611
- return "[Low] **Low Test Coverage**. Very few test files were detected. Recommended to add more unit or integration tests."
612
 
613
  def _detect_features(self, files: list[str], stats: dict) -> dict:
614
  features = {}
615
  files_str = " ".join(files).lower()
616
-
617
- # Authentication - Only if CodeBERT detected Security logic
618
- if stats["Security"] > 0:
619
- auth_indicators = [
620
- "/auth/",
621
- "/login",
622
- "/register",
623
- "passport",
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 "vite" in files_str:
694
- tools.add("Vite")
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]: # Depth 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
- # Create the global instance
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()