Merge pull request #31 from abhigyanpatwari/leidens_algorithm_and_processmap

Leidens algorithm and processmap
This commit is contained in:
Abhigyan Patwari 2026-01-23 19:35:49 +05:30 committed by GitHub
commit d02f861309
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
40 changed files with 16294 additions and 532 deletions

View file

@ -1,17 +1,16 @@
{
"name": "gitnexus-mcp",
"version": "0.1.0",
"version": "0.1.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "gitnexus-mcp",
"version": "0.1.0",
"version": "0.1.1",
"license": "MIT",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.0.0",
"chalk": "^5.3.0",
"commander": "^12.0.0",
"ora": "^8.0.0",
"uuid": "^13.0.0",
"ws": "^8.16.0"
},
"bin": {
@ -19,6 +18,7 @@
},
"devDependencies": {
"@types/node": "^20.0.0",
"@types/uuid": "^10.0.0",
"@types/ws": "^8.5.10",
"tsx": "^4.0.0",
"typescript": "^5.4.0"
@ -530,6 +530,13 @@
"undici-types": "~6.21.0"
}
},
"node_modules/@types/uuid": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz",
"integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/ws": {
"version": "8.18.1",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
@ -586,18 +593,6 @@
}
}
},
"node_modules/ansi-regex": {
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
"integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
}
},
"node_modules/body-parser": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz",
@ -660,54 +655,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/chalk": {
"version": "5.6.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
"integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==",
"license": "MIT",
"engines": {
"node": "^12.17.0 || ^14.13 || >=16.0.0"
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/cli-cursor": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz",
"integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==",
"license": "MIT",
"dependencies": {
"restore-cursor": "^5.0.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/cli-spinners": {
"version": "2.9.2",
"resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz",
"integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==",
"license": "MIT",
"engines": {
"node": ">=6"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/commander": {
"version": "12.1.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz",
"integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==",
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/content-disposition": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz",
@ -821,12 +768,6 @@
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
"license": "MIT"
},
"node_modules/emoji-regex": {
"version": "10.6.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz",
"integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==",
"license": "MIT"
},
"node_modules/encodeurl": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
@ -1087,18 +1028,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-east-asian-width": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz",
"integrity": "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==",
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/get-intrinsic": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
@ -1246,36 +1175,12 @@
"node": ">= 0.10"
}
},
"node_modules/is-interactive": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz",
"integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==",
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/is-promise": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
"integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
"license": "MIT"
},
"node_modules/is-unicode-supported": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz",
"integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==",
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
@ -1303,34 +1208,6 @@
"integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==",
"license": "BSD-2-Clause"
},
"node_modules/log-symbols": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-6.0.0.tgz",
"integrity": "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==",
"license": "MIT",
"dependencies": {
"chalk": "^5.3.0",
"is-unicode-supported": "^1.3.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/log-symbols/node_modules/is-unicode-supported": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz",
"integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==",
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@ -1386,18 +1263,6 @@
"url": "https://opencollective.com/express"
}
},
"node_modules/mimic-function": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz",
"integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==",
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@ -1455,44 +1320,6 @@
"wrappy": "1"
}
},
"node_modules/onetime": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz",
"integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==",
"license": "MIT",
"dependencies": {
"mimic-function": "^5.0.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/ora": {
"version": "8.2.0",
"resolved": "https://registry.npmjs.org/ora/-/ora-8.2.0.tgz",
"integrity": "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==",
"license": "MIT",
"dependencies": {
"chalk": "^5.3.0",
"cli-cursor": "^5.0.0",
"cli-spinners": "^2.9.2",
"is-interactive": "^2.0.0",
"is-unicode-supported": "^2.0.0",
"log-symbols": "^6.0.0",
"stdin-discarder": "^0.2.2",
"string-width": "^7.2.0",
"strip-ansi": "^7.1.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/parseurl": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
@ -1601,22 +1428,6 @@
"url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
}
},
"node_modules/restore-cursor": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz",
"integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==",
"license": "MIT",
"dependencies": {
"onetime": "^7.0.0",
"signal-exit": "^4.1.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/router": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
@ -1783,18 +1594,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/signal-exit": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
"license": "ISC",
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/statuses": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
@ -1804,50 +1603,6 @@
"node": ">= 0.8"
}
},
"node_modules/stdin-discarder": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz",
"integrity": "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==",
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/string-width": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
"integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
"license": "MIT",
"dependencies": {
"emoji-regex": "^10.3.0",
"get-east-asian-width": "^1.0.0",
"strip-ansi": "^7.1.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/strip-ansi": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz",
"integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==",
"license": "MIT",
"dependencies": {
"ansi-regex": "^6.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
}
},
"node_modules/toidentifier": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
@ -1921,6 +1676,19 @@
"node": ">= 0.8"
}
},
"node_modules/uuid": {
"version": "13.0.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz",
"integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"license": "MIT",
"bin": {
"uuid": "dist-node/bin/uuid"
}
},
"node_modules/vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",

View file

@ -1,151 +1,372 @@
# GitNexus V2 Strategic Roadmap: The "Semantic Understanding" Upgrade
# GitNexus V2: Semantic Code Intelligence Roadmap
> **Context for Models:** This document contains a comprehensive analysis of a competitor tool ("Noodlbox") and a detailed roadmap for integrating its best features into GitNexus. GitNexus is a browser-native (WASM) code analysis tool using KuzuDB and Tree-sitter. This roadmap aims to elevate GitNexus from a "Code Graph" to a "Semantic Code Understanding" platform.
> **Last Updated:** January 2026
> **Vision:** Transform GitNexus from a "Code Graph" into a "Semantic Code Understanding" platform that rivals and surpasses tools like Noodlbox and DeepWiki.
---
## 1. Competitive Analysis: GitNexus vs. Noodlbox
## Executive Summary
### Core Architecture
| Feature | GitNexus (Current) | Noodlbox (Competitor) | Advantage |
|---------|-------------------|-----------------------|-----------|
| **Runtime** | **100% Browser (WASM)**. Zero setup. Privacy-first. | **CLI + Local Server**. Requires installation/daemon. | **GitNexus** (UX/Privacy) |
| **Storage** | **KuzuDB (WASM)**. Graph + Vector. | **LanceDB**. Graph + Vector. | Tied (Both robust) |
| **Extraction** | Tree-sitter (WASM). | Tree-sitter. | Tied |
| **Integration** | Custom MCP Bridge (Browser ↔ Local). | Direct CLI MCP Server. | Noodlbox (Simpler setup) |
### Current State
- ✅ Tree-sitter parsing → AST extraction
- ✅ KuzuDB (WASM) → Graph + Vector storage
- ✅ Hybrid search (BM25 + Semantic)
- ✅ LangChain Agent with tools (search, cypher, grep, read, blastRadius, highlight)
- ✅ MCP integration for external AI tools
### Conceptual Model (The Gap)
| Feature | GitNexus | Noodlbox | Analysis |
|---------|----------|----------|----------|
| **Grouping** | File-based (Folders). | **Communities**. Algorithmic clustering of related code. | **Noodlbox**. Reveals logical architecture vs physical. |
| **Flows** | Raw `CALLS` edges. | **Processes**. Named execution paths (Entry → End). | **Noodlbox**. "How it works" vs "What calls what". |
| **Search** | Hybrid (BM25 + Vec). | Context Search. | Tied, but Noodlbox returns Processes. |
| **Impact** | Node Blast Radius. | **Git Diff Impact**. Unstaged/Staged analysis. | **Noodlbox**. Much better DevEx. |
| **UX** | Raw IDs/Filenames. | **Labels**. Human-readable names ("Auth System"). | **Noodlbox**. Cognitive load reduction. |
### Target State
- 🎯 **Communities:** Auto-detected code clusters (Leiden algorithm)
- 🎯 **Processes:** Named execution flows with ordered steps
- 🎯 **Hierarchical Navigation:** Codebase → Community → Process → Symbol
- 🎯 **Auto-Documentation:** Generate ARCHITECTURE/ docs from graph
- 🎯 **Incremental Updates:** File watch + delta graph updates
- 🎯 **Git Diff Impact:** Pre-commit blast radius on uncommitted changes
---
## 2. Strategic Goal: "The Graph of Meaning"
We need to move GitNexus beyond just mapping *files and functions* to mapping *concepts and flows*.
**Shift:** `Files -> Communities`, `Functions -> Processes`, `Edges -> Flows`.
## Competitive Analysis
### vs Noodlbox
| Feature | Noodlbox | GitNexus (Current) | GitNexus (Planned) |
|---------|----------|-------------------|-------------------|
| Runtime | CLI + Server | Browser (WASM) ✅ | Browser (WASM) ✅ |
| Communities | Leiden clusters ✅ | ❌ | ✅ Planned |
| Processes | Named flows ✅ | ❌ | ✅ Planned |
| Git Diff Impact | ✅ | ❌ | ✅ Planned |
| Privacy | Local server | 100% Browser ✅ | 100% Browser ✅ |
### vs OpenDeepWiki
| Feature | OpenDeepWiki | GitNexus (Planned) |
|---------|--------------|-------------------|
| Structure Discovery | LLM guesses from files | Leiden from actual relationships ✅ |
| Process Understanding | None (file-by-file) | Static analysis traces ✅ |
| Grounding | File references only | Graph edges + files ✅ |
| "What breaks if X changes" | Cannot answer | blastRadius ✅ |
**Our Advantage:** Real graph-based understanding vs LLM inference.
---
## 3. Implementation Roadmap
### 🔥 PHASE 1: The Semantic Layer (High Priority)
*Transform the raw KuzuDB graph into high-level concepts.*
#### 1.1 Communities (Code Clusters)
**Concept:** Use modularity optimization to group tightly coupled symbols into "Communities" (e.g., "Auth System", "Payment Processing") regardless of folder structure.
* **Algorithm:** Implement **Leiden Algorithm** (superior to Louvain for refined clusters) or **Louvain** in WASM (or pure JS if graph small enough).
* **Trigger:** Run post-ingestion.
* **Schema Update:**
* New Node: `Community { id, label, cohesionScore }`
* New Edge: `MEMBER_OF` (Symbol -> Community)
* **Metrics:** Calculate **Cohesion Score** (internal edges / total edges) to rate cluster quality (0-1).
* **Key Symbols:** Identify diverse "Entry Points" (called from outside) and "Central Hubs" (high internal degree).
#### 1.2 Processes (Execution Flows)
**Concept:** Trace and name execution paths often hidden in call graphs.
* **Detection Heuristic:**
1. Find **Entry Points**: Functions called by Frameworks (e.g., `handleRequest`) or with 0 internal callers.
2. Trace forward `CALLS` edges (limit depth/branching).
3. Group linear paths.
* **Schema Update:**
* New Node: `Process { id, label, process_type: 'intra'|'cross' }`
* New Edge: `STEP_IN_PROCESS { step_number }` (Symbol -> Process)
* **Process Labeling:** Heuristic naming (e.g., `[EntryFn]_[Action]`).
#### 1.3 Git Diff Impact Detection
**Concept:** Analyze *work-in-progress* code, not just the committed graph.
* **MCP Tool:** `detect_impact(scope: 'unstaged' | 'staged')`
* **Workflow:**
1. MCP Client reads `git diff`.
2. Parse diff to find changed symbols (e.g., `src/auth.ts: function login`).
3. Query Graph for `potentially_affected` (Downstream callers).
4. **Crucial:** Map affected symbols to **Processes** and **Communities**.
5. Output: "Changing `login` affects 'Checkout Flow' and 'User Onboarding Process'".
## Phase-Wise Implementation Plan
---
### 🟡 PHASE 2: Cognitive UX (Medium Priority)
*Make the complex graph human-readable.*
## PHASE 1: Community Detection (Leiden Algorithm)
**Goal:** Group related code into named clusters.
#### 2.1 Smart Labeling
**Concept:** Replacing IDs with meaningful names.
* **Generator:** Simple LLM pass or rule-based heuristic.
* Input: Top file paths in Community (e.g., `src/auth/*`, `src/login/*`).
* Output: Label "Authentication".
* **Storage:** `.gitnexus/labels.json`.
* **Usage:** UI & MCP tools display "Auth System" instead of "Community #12".
### 1.1 Research & Setup
- [ ] Research JS/WASM implementations of Leiden algorithm
- Options: `graphology-communities-louvain`, custom WASM port
- Constraint: Must run in browser
- [ ] Benchmark on sample codebases (100, 1K, 10K nodes)
#### 2.2 Architecture Generation (`generate_map`)
**Concept:** Auto-generated documentation that stays up to date.
* **MCP Tool:** `gitnexus_generate_architecture`
* **Output:** `ARCHITECTURE/` folder.
* `README.md`: Mermaid Diagram of Communities + Cross-community data flow.
* `{process_name}.md`: Sequence diagrams of key processes.
* **Value:** Instant "Onboarding Docs" for any repo.
### 1.2 Schema Updates
- [ ] Add `Community` node table to KuzuDB schema:
```typescript
interface Community {
id: string; // "comm_a7f3x2"
label: string; // "Authentication" (heuristic or LLM)
cohesion: number; // 0.0 - 1.0
symbolCount: number; // Count of symbols in community
}
```
- [ ] Add `MEMBER_OF` relationship type to `CodeRelation`:
```typescript
// Symbol -> Community
{ type: 'MEMBER_OF', source: symbolId, target: communityId }
```
#### 2.3 Centrality & Importance
**Concept:** Not all nodes are equal.
* **Algorithm:** **PageRank** or **Betweenness Centrality**.
* **Use Case:** Search results ranking. When searching "Auth", show the `AuthService` class before a random utility function used by it.
### 1.3 Ingestion Pipeline Update
- [ ] Create `community-processor.ts` in `src/core/ingestion/`
- [ ] Add Phase 6 to pipeline (after heritage processing):
```typescript
// pipeline.ts
await processCommunities(graph, onProgress);
```
- [ ] Implement Leiden on the CALLS + IMPORTS adjacency matrix
- [ ] Generate heuristic labels (folder name majority)
### 1.4 Agent Integration
- [ ] Add `listCommunities` tool or resource
- [ ] Update system prompt to teach agent about communities
- [ ] Update `search` tool to return community context
### 1.5 UI Updates
- [ ] Color nodes by community in graph visualization
- [ ] Add community filter/legend panel
**Estimated Effort:** 2-3 weeks
---
### 🟢 PHASE 3: Agentic Workflows (Low Priority)
*Standardized "Thinking Patterns" for the LLM.*
## PHASE 2: Process Detection (Execution Flows)
**Goal:** Trace and name execution paths.
#### 3.1 "Skills" (Structured Prompts)
Define rigid workflows for the Agent to prevent "wandering":
* **Exploration Skill:** `Read Map -> Pick Community -> List Key Processes -> Drill Down`.
* **Debugging Skill:** `Error Msg -> Search Context -> Trace Backwards (Callers) -> Check Recent Changes`.
* **Refactoring Skill:** `Select Symbol -> Blast Radius -> Dependencies (In/Out) -> Plan Split`.
### 2.1 Schema Updates
- [ ] Add `Process` node table to KuzuDB schema:
```typescript
interface Process {
id: string; // "proc_login_flow"
label: string; // "User Login Flow"
type: 'intra_community' | 'cross_community';
stepCount: number;
}
```
- [ ] Add `STEP_IN_PROCESS` relationship type:
```typescript
// Symbol -> Process (with step property)
{ type: 'STEP_IN_PROCESS', source: symbolId, target: processId, step: number }
```
#### 3.2 Hooks
* **Session Hook:** auto-inject `database-schema` and high-level `codebase-map` at chat start.
* **Search Hook:** Intercept `grep` in IDE to attach semantic context ("This match is part of 'Payment Process'").
### 2.2 Process Detection Algorithm
- [ ] Create `process-processor.ts` in `src/core/ingestion/`
- [ ] Implement entry point detection:
```typescript
// Functions with no internal callers
MATCH (f:Function)
WHERE NOT (:Function)-[:CALLS]->(f)
RETURN f
```
- [ ] Implement forward tracing (BFS/DFS from entry points)
- [ ] Limit depth (e.g., 10) and branching (e.g., 3)
- [ ] Deduplicate overlapping paths
- [ ] Label processes (heuristic: `{entry}_to_{terminal}`)
### 2.3 Community Integration
- [ ] Track which communities each process touches
- [ ] Mark `type` as `cross_community` if > 1 community
### 2.4 Agent Integration
- [ ] Add `listProcesses(communityId?)` tool
- [ ] Add `traceProcess(processId)` tool
- [ ] Update system prompt with process navigation
### 2.5 UI Updates
- [ ] Visualize processes as highlighted paths
- [ ] Add process list panel
- [ ] Click process → animate the flow
**Estimated Effort:** 2 weeks
---
## 4. Technical Specs & Schema Changes
## PHASE 3: Smart Labeling (LLM Enhancement)
**Goal:** Human-readable names for Communities and Processes.
### Proposed KuzuDB Schema V2
### 3.1 Heuristic Labeling (Default)
- [ ] Community: Most common folder prefix
- [ ] Process: `{entryFunction}_to_{terminalFunction}`
### 3.2 LLM Labeling (Optional Enhancement)
- [ ] Create `labeling-service.ts`
- [ ] Batch communities/processes for LLM naming
- [ ] Prompt template:
```
Given these functions: login, validateToken, checkExpiry, refreshSession
All in folder: src/auth/
Generate a 2-3 word label for this code cluster.
```
- [ ] Store both `heuristicLabel` and `llmLabel`
- [ ] Use `llmLabel` if available, else `heuristicLabel`
### 3.3 Labels File Export
- [ ] Generate `.gitnexus/labels.json` on demand
- [ ] Format matching Noodlbox for familiarity
**Estimated Effort:** 1 week
---
## PHASE 4: Architecture Documentation Generation
**Goal:** Auto-generate project documentation from graph.
### 4.1 Documentation Structure
- [ ] Output: `ARCHITECTURE/` folder
```
ARCHITECTURE/
├── README.md # Overview + Mermaid diagram
├── communities/
│ ├── authentication.md # Community detail
│ └── payments.md
└── processes/
├── user-login-flow.md # Process trace
└── checkout-flow.md
```
### 4.2 Implementation
- [ ] Create `generate-docs.ts` in `src/core/docs/`
- [ ] README.md generation:
- Codebase stats (files, symbols, communities, processes)
- Mermaid diagram of community relationships
- List of key processes
- [ ] Community doc generation:
- Key symbols (highest centrality)
- Entry points
- Processes in this community
- [ ] Process doc generation:
- Ordered step list with file paths
- Mermaid sequence diagram
- Cross-community markers
### 4.3 MCP Tool
- [ ] Add `generateArchitecture` tool to MCP
- [ ] Returns generated markdown (or writes to files)
**Estimated Effort:** 2 weeks
---
## PHASE 5: Git Diff Impact Detection
**Goal:** Pre-commit blast radius analysis.
### 5.1 MCP Server Updates
- [ ] Add `detectImpact` tool to `gitnexus-mcp`
- [ ] Parameters:
```typescript
interface DetectImpactParams {
scope: 'unstaged' | 'staged' | 'all' | 'compare';
baseRef?: string; // For 'compare' scope
}
```
### 5.2 Implementation
- [ ] Run `git diff` (MCP server side)
- [ ] Parse diff to extract changed file paths + line ranges
- [ ] Map changes to symbols in graph
- [ ] Run `blastRadius` on each changed symbol
- [ ] Aggregate results by:
- Changed symbols
- Impacted processes
- Affected communities
- Risk level (low/medium/high)
### 5.3 Response Format
```typescript
// Nodes
interface Community {
id: string; // "comm_1"
label: string; // "Authentication"
cohesion: number; // 0.85
interface ImpactResult {
changedSymbols: { name: string; file: string; changeType: 'added' | 'modified' | 'deleted' }[];
impactedProcesses: { id: string; label: string; affectedSteps: number[] }[];
affectedCommunities: string[];
riskLevel: 'low' | 'medium' | 'high';
}
interface Process {
id: string; // "proc_login_flow"
label: string; // "Login Flow"
type: 'intra_community' | 'cross_community';
}
// Relationships
// MEMBER_OF: Symbol -> Community
// STEP_IN_PROCESS: Symbol -> Process (property: step_index)
```
### Algorithm Reference
* **Leiden Algorithm:**
* *Input:* Adjacency matrix of `CALLS` + `EXTENDS` edges.
* *Output:* Partition assignment (NodeID -> CommunityID).
* *Constraint:* Must run fast in JS/WASM.
* **Reciprocal Rank Fusion (RRF):**
* Already used for Search. Can be refined to weight "Key Symbols" (Centrality) higher.
**Estimated Effort:** 1-2 weeks
---
## 5. Summary of Work to Be Done
1. **Ingestion:** Add `CommunityDetection` and `ProcessTracing` post-processors.
2. **Schema:** Add `Community` and `Process` tables.
3. **MCP:** Add `detect_impact` tool accepting git diffs.
4. **UI:** Visualize Communities (colored clusters) and Processes (animated paths).
5. **Docs:** Implement `generate_map` to export findings to Markdown.
## PHASE 6: Incremental Updates (File Watch)
**Goal:** Real-time graph updates on file changes.
### 6.1 File Watching
- [ ] Integrate file system watcher in MCP server
- [ ] Detect: added, modified, deleted files
- [ ] Debounce rapid changes (e.g., 500ms)
### 6.2 Incremental Parsing
- [ ] Hash-based cache: file path → content hash → AST
- [ ] On change: re-parse only changed file
- [ ] Compute delta: added/removed nodes and edges
### 6.3 Graph Patching
- [ ] Add `patchGraph` method to KuzuDB adapter
- [ ] Operations: ADD_NODE, REMOVE_NODE, ADD_EDGE, REMOVE_EDGE
- [ ] Update affected communities (optional: re-run Leiden locally)
- [ ] Update affected processes (re-trace from changed symbols)
### 6.4 Re-embedding
- [ ] Re-embed changed symbols
- [ ] Re-embed 1-hop neighbors (context changed)
**Estimated Effort:** 2-3 weeks
---
## PHASE 7: Agent Prompt Refinement
**Goal:** Teach agent to use hierarchical navigation.
### 7.1 System Prompt Updates
- [ ] Add "Hierarchical Navigation Protocol":
```
1. Query codebase map (communities overview)
2. Identify relevant community
3. List processes in that community
4. Trace specific process
5. Read code for specific steps
```
- [ ] Add tool descriptions for new tools
### 7.2 Skills (Optional)
- [ ] Create structured prompts for common tasks:
- Exploration: "How does X work?"
- Debugging: "Why is X failing?"
- Refactoring: "What breaks if I change X?"
- Documentation: "Generate docs for X"
**Estimated Effort:** 1 week
---
## Implementation Priority Order
| Phase | Name | Priority | Effort | Dependency |
|-------|------|----------|--------|------------|
| 1 | Communities (Leiden) | 🔴 Critical | 2-3 weeks | None |
| 2 | Processes | 🔴 Critical | 2 weeks | Phase 1 |
| 3 | Smart Labeling | 🟡 Medium | 1 week | Phase 1, 2 |
| 4 | Documentation | 🟡 Medium | 2 weeks | Phase 1, 2 |
| 5 | Git Diff Impact | 🟡 Medium | 1-2 weeks | None (uses existing blastRadius) |
| 6 | Incremental Updates | 🟢 Nice-to-have | 2-3 weeks | None |
| 7 | Agent Prompt | 🟡 Medium | 1 week | Phase 1, 2 |
**Recommended order:** 1 → 2 → 7 → 3 → 4 → 5 → 6
---
## Technical Notes
### Leiden Algorithm Options
1. **graphology-communities-louvain** (JS, works in browser)
2. **Custom WASM port** (if performance needed)
3. **Simple Louvain** might be sufficient for V1
### Schema Summary (New Additions)
```
NEW NODES:
- Community { id, label, cohesion, symbolCount }
- Process { id, label, type, stepCount }
NEW RELATIONSHIPS (in CodeRelation):
- MEMBER_OF: Symbol → Community
- STEP_IN_PROCESS: Symbol → Process (with step property)
```
### Graph Visualization Color Scheme
```
Community colors (auto-assigned):
- Auth: Blue
- Data: Green
- API: Orange
- Payment: Purple
- ... (cyclic palette)
```
---
## Success Metrics
| Metric | Current | Target |
|--------|---------|--------|
| Agent query accuracy | ~70% | 90%+ |
| Context tokens per query | High (dump everything) | Low (hierarchical zoom) |
| Documentation quality | N/A | Comparable to DeepWiki |
| Update latency | Full re-ingest (minutes) | Incremental (seconds) |
---
## Next Steps
1. **Immediate:** Research Leiden implementations for browser
2. **Week 1-2:** Implement Phase 1 (Communities)
3. **Week 3-4:** Implement Phase 2 (Processes)
4. **Week 5:** Refine agent prompt (Phase 7)
5. **Week 6+:** Documentation generation & polish

View file

@ -23,6 +23,7 @@
"comlink": "^4.4.2",
"d3": "^7.9.0",
"graphology": "^0.26.0",
"graphology-communities-louvain": "^2.0.2",
"graphology-layout-force": "^0.2.4",
"graphology-layout-forceatlas2": "^0.10.1",
"graphology-layout-noverlap": "^0.4.2",
@ -56,6 +57,7 @@
"@types/react-syntax-highlighter": "^15.5.13",
"@vercel/node": "^5.5.16",
"@vitejs/plugin-react": "^5.1.0",
"tree-sitter-wasms": "^0.1.13",
"typescript": "^5.4.5",
"vite": "^5.2.0",
"vite-plugin-static-copy": "^3.1.4"
@ -5515,6 +5517,34 @@
"graphology-types": ">=0.24.0"
}
},
"node_modules/graphology-communities-louvain": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/graphology-communities-louvain/-/graphology-communities-louvain-2.0.2.tgz",
"integrity": "sha512-zt+2hHVPYxjEquyecxWXoUoIuN/UvYzsvI7boDdMNz0rRvpESQ7+e+Ejv6wK7AThycbZXuQ6DkG8NPMCq6XwoA==",
"license": "MIT",
"dependencies": {
"graphology-indices": "^0.17.0",
"graphology-utils": "^2.4.4",
"mnemonist": "^0.39.0",
"pandemonium": "^2.4.1"
},
"peerDependencies": {
"graphology-types": ">=0.19.0"
}
},
"node_modules/graphology-indices": {
"version": "0.17.0",
"resolved": "https://registry.npmjs.org/graphology-indices/-/graphology-indices-0.17.0.tgz",
"integrity": "sha512-A7RXuKQvdqSWOpn7ZVQo4S33O0vCfPBnUSf7FwE0zNCasqwZVUaCXePuWo5HBpWw68KJcwObZDHpFk6HKH6MYQ==",
"license": "MIT",
"dependencies": {
"graphology-utils": "^2.4.2",
"mnemonist": "^0.39.0"
},
"peerDependencies": {
"graphology-types": ">=0.20.0"
}
},
"node_modules/graphology-layout-force": {
"version": "0.2.4",
"resolved": "https://registry.npmjs.org/graphology-layout-force/-/graphology-layout-force-0.2.4.tgz",
@ -7726,6 +7756,15 @@
"ufo": "^1.6.1"
}
},
"node_modules/mnemonist": {
"version": "0.39.8",
"resolved": "https://registry.npmjs.org/mnemonist/-/mnemonist-0.39.8.tgz",
"integrity": "sha512-vyWo2K3fjrUw8YeeZ1zF0fy6Mu59RHokURlld8ymdUPjMlD9EC9ov1/YPqTgqRvUN9nTr3Gqfz29LYAmu0PHPQ==",
"license": "MIT",
"dependencies": {
"obliterator": "^2.0.1"
}
},
"node_modules/mri": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz",
@ -7844,6 +7883,12 @@
"node": ">= 0.4"
}
},
"node_modules/obliterator": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/obliterator/-/obliterator-2.0.5.tgz",
"integrity": "sha512-42CPE9AhahZRsMNslczq0ctAEtqk8Eka26QofnqC346BZdHDySk3LWka23LI7ULIw11NmltpiLagIq8gBozxTw==",
"license": "MIT"
},
"node_modules/observable-fns": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/observable-fns/-/observable-fns-0.6.1.tgz",
@ -8009,6 +8054,15 @@
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
"license": "(MIT AND Zlib)"
},
"node_modules/pandemonium": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/pandemonium/-/pandemonium-2.4.1.tgz",
"integrity": "sha512-wRqjisUyiUfXowgm7MFH2rwJzKIr20rca5FsHXCMNm1W5YPP1hCtrZfgmQ62kP7OZ7Xt+cR858aB28lu5NX55g==",
"license": "MIT",
"dependencies": {
"mnemonist": "^0.39.2"
}
},
"node_modules/parse-entities": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz",
@ -9101,6 +9155,16 @@
"dev": true,
"license": "MIT"
},
"node_modules/tree-sitter-wasms": {
"version": "0.1.13",
"resolved": "https://registry.npmjs.org/tree-sitter-wasms/-/tree-sitter-wasms-0.1.13.tgz",
"integrity": "sha512-wT+cR6DwaIz80/vho3AvSF0N4txuNx/5bcRKoXouOfClpxh/qqrF4URNLQXbbt8MaAxeksZcZd1j8gcGjc+QxQ==",
"dev": true,
"license": "Unlicense",
"dependencies": {
"tree-sitter-wasms": "^0.1.11"
}
},
"node_modules/trim-lines": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz",

View file

@ -24,6 +24,7 @@
"comlink": "^4.4.2",
"d3": "^7.9.0",
"graphology": "^0.26.0",
"graphology-communities-louvain": "^2.0.2",
"graphology-layout-force": "^0.2.4",
"graphology-layout-forceatlas2": "^0.10.1",
"graphology-layout-noverlap": "^0.4.2",
@ -57,8 +58,9 @@
"@types/react-syntax-highlighter": "^15.5.13",
"@vercel/node": "^5.5.16",
"@vitejs/plugin-react": "^5.1.0",
"tree-sitter-wasms": "^0.1.13",
"typescript": "^5.4.5",
"vite": "^5.2.0",
"vite-plugin-static-copy": "^3.1.4"
}
}
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -1,4 +1,4 @@
import { useCallback, useRef } from 'react';
import { useCallback, useRef, useState, useEffect } from 'react';
import { AppStateProvider, useAppState } from './hooks/useAppState';
import { DropZone } from './components/DropZone';
import { LoadingOverlay } from './components/LoadingOverlay';
@ -11,6 +11,8 @@ import { FileTreePanel } from './components/FileTreePanel';
import { CodeReferencesPanel } from './components/CodeReferencesPanel';
import { FileEntry } from './services/zip';
import { getActiveProviderConfig } from './core/llm/settings-service';
import { ProviderConfig } from './core/llm/types';
import { IntelligentClusteringModal } from './components/IntelligentClusteringModal';
const AppContent = () => {
const {
@ -29,23 +31,68 @@ const AppContent = () => {
refreshLLMSettings,
initializeAgent,
startEmbeddings,
startBackgroundEnrichment,
embeddingStatus,
codeReferences,
selectedNode,
isCodePanelOpen,
llmSettings,
updateLLMSettings,
runClusterEnrichment,
} = useAppState();
const [showClusteringModal, setShowClusteringModal] = useState(false);
// Trigger clustering modal after ingestion if not seen yet
// DISABLED: Clustering is now in the upload flow
/*
useEffect(() => {
if (viewMode === 'exploring' && !llmSettings.hasSeenClusteringPrompt && !llmSettings.intelligentClustering) {
const timer = setTimeout(() => setShowClusteringModal(true), 2000);
return () => clearTimeout(timer);
}
}, [viewMode, llmSettings.hasSeenClusteringPrompt, llmSettings.intelligentClustering]);
*/
const handleEnableClustering = useCallback(() => {
updateLLMSettings({
intelligentClustering: true,
hasSeenClusteringPrompt: true,
useSameModelForClustering: true // Default to simple path
});
setShowClusteringModal(false);
runClusterEnrichment().catch(console.error);
}, [updateLLMSettings, runClusterEnrichment]);
const handleConfigureClustering = useCallback(() => {
updateLLMSettings({ hasSeenClusteringPrompt: true });
setShowClusteringModal(false);
setSettingsPanelOpen(true);
}, [updateLLMSettings, setSettingsPanelOpen]);
const handleSkipClustering = useCallback(() => {
updateLLMSettings({ hasSeenClusteringPrompt: true });
setShowClusteringModal(false);
}, [updateLLMSettings]);
const graphCanvasRef = useRef<GraphCanvasHandle>(null);
const handleFileSelect = useCallback(async (file: File) => {
const handleFileSelect = useCallback(async (file: File, enableSmartClustering?: boolean) => {
console.log('📥 App.handleFileSelect - param received:', enableSmartClustering, 'provider exists:', !!getActiveProviderConfig());
const projectName = file.name.replace('.zip', '');
setProjectName(projectName);
// Set initial progress BEFORE entering loading mode to prevent black screen
setProgress({ phase: 'extracting', percent: 0, message: 'Starting...', detail: 'Preparing to extract files' });
setViewMode('loading');
try {
// Prepare LLM config if clustering is enabled
const clusteringConfig = enableSmartClustering ? getActiveProviderConfig() ?? undefined : undefined;
console.log('✅ clusteringConfig:', !!clusteringConfig, clusteringConfig?.provider);
const result = await runPipeline(file, (progress) => {
setProgress(progress);
});
}, clusteringConfig || undefined);
setGraph(result.graph);
setFileContents(result.fileContents);
@ -67,6 +114,9 @@ const AppContent = () => {
console.warn('Embeddings auto-start failed:', err);
}
});
// Start background cluster enrichment (if toggle was enabled)
startBackgroundEnrichment().catch(console.warn);
} catch (error) {
console.error('Pipeline error:', error);
setProgress({
@ -80,20 +130,25 @@ const AppContent = () => {
setProgress(null);
}, 3000);
}
}, [setViewMode, setGraph, setFileContents, setProgress, setProjectName, runPipeline, startEmbeddings, initializeAgent]);
}, [setViewMode, setGraph, setFileContents, setProgress, setProjectName, runPipeline, startEmbeddings, initializeAgent, llmSettings]);
const handleGitClone = useCallback(async (files: FileEntry[]) => {
const handleGitClone = useCallback(async (files: FileEntry[], enableSmartClustering?: boolean) => {
// Extract project name from first file path (e.g., "owner-repo-123/src/..." -> "owner-repo")
const firstPath = files[0]?.path || 'repository';
const projectName = firstPath.split('/')[0].replace(/-\d+$/, '') || 'repository';
setProjectName(projectName);
// Set initial progress BEFORE entering loading mode to prevent black screen
setProgress({ phase: 'extracting', percent: 0, message: 'Starting...', detail: 'Preparing to process files' });
setViewMode('loading');
try {
// Prepare LLM config if clustering is enabled
const clusteringConfig = enableSmartClustering ? getActiveProviderConfig() ?? undefined : undefined;
const result = await runPipelineFromFiles(files, (progress) => {
setProgress(progress);
});
}, clusteringConfig || undefined);
setGraph(result.graph);
setFileContents(result.fileContents);
@ -115,6 +170,9 @@ const AppContent = () => {
console.warn('Embeddings auto-start failed:', err);
}
});
// Start background cluster enrichment (if toggle was enabled)
startBackgroundEnrichment().catch(console.warn);
} catch (error) {
console.error('Pipeline error:', error);
setProgress({
@ -128,7 +186,7 @@ const AppContent = () => {
setProgress(null);
}, 3000);
}
}, [setViewMode, setGraph, setFileContents, setProgress, setProjectName, runPipelineFromFiles, startEmbeddings, initializeAgent]);
}, [setViewMode, setGraph, setFileContents, setProgress, setProjectName, runPipelineFromFiles, startEmbeddings, initializeAgent, runClusterEnrichment]);
const handleFocusNode = useCallback((nodeId: string) => {
graphCanvasRef.current?.focusNode(nodeId);
@ -183,6 +241,14 @@ const AppContent = () => {
onClose={() => setSettingsPanelOpen(false)}
onSettingsSaved={handleSettingsSaved}
/>
{/* Intelligent Clustering Modal */}
<IntelligentClusteringModal
isOpen={showClusteringModal}
onClose={handleSkipClustering}
onEnable={handleEnableClustering}
onConfigure={handleConfigureClustering}
/>
</div>
);
};

View file

@ -1,11 +1,12 @@
import { useState, useCallback, DragEvent } from 'react';
import { Upload, FileArchive, Github, Loader2, ArrowRight, Key, Eye, EyeOff } from 'lucide-react';
import { useState, useCallback, DragEvent, useEffect } from 'react';
import { Upload, FileArchive, Github, Loader2, ArrowRight, Key, Eye, EyeOff, Sparkles } from 'lucide-react';
import { cloneRepository, parseGitHubUrl } from '../services/git-clone';
import { FileEntry } from '../services/zip';
import { getActiveProviderConfig } from '../core/llm/settings-service';
interface DropZoneProps {
onFileSelect: (file: File) => void;
onGitClone?: (files: FileEntry[]) => void;
onFileSelect: (file: File, enableSmartClustering?: boolean) => void;
onGitClone?: (files: FileEntry[], enableSmartClustering?: boolean) => void;
}
export const DropZone = ({ onFileSelect, onGitClone }: DropZoneProps) => {
@ -17,6 +18,15 @@ export const DropZone = ({ onFileSelect, onGitClone }: DropZoneProps) => {
const [isCloning, setIsCloning] = useState(false);
const [cloneProgress, setCloneProgress] = useState({ phase: '', percent: 0 });
const [error, setError] = useState<string | null>(null);
const [enableSmartClustering, setEnableSmartClustering] = useState(false);
const [hasLLMProvider, setHasLLMProvider] = useState(false);
// Check if LLM provider is configured
useEffect(() => {
const config = getActiveProviderConfig();
setHasLLMProvider(!!config);
// Keep smart clustering OFF by default, user must opt-in
}, []);
const handleDragOver = useCallback((e: DragEvent<HTMLDivElement>) => {
e.preventDefault();
@ -39,24 +49,25 @@ export const DropZone = ({ onFileSelect, onGitClone }: DropZoneProps) => {
if (files.length > 0) {
const file = files[0];
if (file.name.endsWith('.zip')) {
onFileSelect(file);
onFileSelect(file, enableSmartClustering);
} else {
setError('Please drop a .zip file');
}
}
}, [onFileSelect]);
}, [onFileSelect, enableSmartClustering]);
const handleFileInput = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
const files = e.target.files;
if (files && files.length > 0) {
const file = files[0];
if (file.name.endsWith('.zip')) {
onFileSelect(file);
console.log('🎯 DropZone: Calling onFileSelect with enableSmartClustering:', enableSmartClustering);
onFileSelect(file, enableSmartClustering);
} else {
setError('Please select a .zip file');
}
}
}, [onFileSelect]);
}, [onFileSelect, enableSmartClustering]);
const handleGitClone = async () => {
if (!githubUrl.trim()) {
@ -76,16 +87,16 @@ export const DropZone = ({ onFileSelect, onGitClone }: DropZoneProps) => {
try {
const files = await cloneRepository(
githubUrl,
githubUrl,
(phase, percent) => setCloneProgress({ phase, percent }),
githubToken || undefined // Pass token if provided
);
// Clear token from memory after successful clone
setGithubToken('');
if (onGitClone) {
onGitClone(files);
onGitClone(files, enableSmartClustering);
}
} catch (err) {
console.error('Clone failed:', err);
@ -123,8 +134,8 @@ export const DropZone = ({ onFileSelect, onGitClone }: DropZoneProps) => {
className={`
flex-1 flex items-center justify-center gap-2 py-2.5 px-4 rounded-lg
text-sm font-medium transition-all duration-200
${activeTab === 'zip'
? 'bg-accent text-white shadow-md'
${activeTab === 'zip'
? 'bg-accent text-white shadow-md'
: 'text-text-secondary hover:text-text-primary hover:bg-elevated'
}
`}
@ -137,8 +148,8 @@ export const DropZone = ({ onFileSelect, onGitClone }: DropZoneProps) => {
className={`
flex-1 flex items-center justify-center gap-2 py-2.5 px-4 rounded-lg
text-sm font-medium transition-all duration-200
${activeTab === 'github'
? 'bg-accent text-white shadow-md'
${activeTab === 'github'
? 'bg-accent text-white shadow-md'
: 'text-text-secondary hover:text-text-primary hover:bg-elevated'
}
`}
@ -157,60 +168,98 @@ export const DropZone = ({ onFileSelect, onGitClone }: DropZoneProps) => {
{/* ZIP Upload Tab */}
{activeTab === 'zip' && (
<div
className={`
relative p-16
bg-surface border-2 border-dashed rounded-3xl
transition-all duration-300 cursor-pointer
${isDragging
? 'border-accent bg-elevated scale-105 shadow-glow'
: 'border-border-default hover:border-accent/50 hover:bg-elevated/50 animate-breathe'
}
`}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
onClick={() => document.getElementById('file-input')?.click()}
>
<input
id="file-input"
type="file"
accept=".zip"
className="hidden"
onChange={handleFileInput}
/>
<>
<div
className={`
relative p-16
bg-surface border-2 border-dashed rounded-3xl
transition-all duration-300 cursor-pointer
${isDragging
? 'border-accent bg-elevated scale-105 shadow-glow'
: 'border-border-default hover:border-accent/50 hover:bg-elevated/50 animate-breathe'
}
`}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
onClick={() => document.getElementById('file-input')?.click()}
>
<input
id="file-input"
type="file"
accept=".zip"
className="hidden"
onChange={handleFileInput}
/>
{/* Icon */}
<div className={`
mx-auto w-20 h-20 mb-6
flex items-center justify-center
bg-gradient-to-br from-accent to-node-interface
rounded-2xl shadow-glow
transition-transform duration-300
${isDragging ? 'scale-110' : ''}
`}>
{isDragging ? (
<Upload className="w-10 h-10 text-white" />
) : (
<FileArchive className="w-10 h-10 text-white" />
)}
{/* Icon */}
<div className={`
mx-auto w-20 h-20 mb-6
flex items-center justify-center
bg-gradient-to-br from-accent to-node-interface
rounded-2xl shadow-glow
transition-transform duration-300
${isDragging ? 'scale-110' : ''}
`}>
{isDragging ? (
<Upload className="w-10 h-10 text-white" />
) : (
<FileArchive className="w-10 h-10 text-white" />
)}
</div>
{/* Text */}
<h2 className="text-xl font-semibold text-text-primary text-center mb-2">
{isDragging ? 'Drop it here!' : 'Drop your codebase'}
</h2>
<p className="text-sm text-text-secondary text-center mb-6">
Drag & drop a .zip file to generate a knowledge graph
</p>
{/* Hints */}
<div className="flex items-center justify-center gap-3 text-xs text-text-muted">
<span className="px-3 py-1.5 bg-elevated border border-border-subtle rounded-md">
.zip
</span>
</div>
</div>
{/* Text */}
<h2 className="text-xl font-semibold text-text-primary text-center mb-2">
{isDragging ? 'Drop it here!' : 'Drop your codebase'}
</h2>
<p className="text-sm text-text-secondary text-center mb-6">
Drag & drop a .zip file to generate a knowledge graph
</p>
{/* Hints */}
<div className="flex items-center justify-center gap-3 text-xs text-text-muted">
<span className="px-3 py-1.5 bg-elevated border border-border-subtle rounded-md">
.zip
</span>
{/* Smart Clustering Toggle - Below drop zone */}
<div className="mt-4 p-4 bg-surface/50 border border-border-subtle rounded-xl">
<label className="flex items-center justify-between cursor-pointer group">
<div className="flex-1">
<div className="flex items-center gap-2 mb-1">
<Sparkles className="w-4 h-4 text-accent" />
<span className="text-sm font-medium text-text-primary">Enable Smart Clustering</span>
</div>
<p className="text-xs text-text-muted leading-relaxed">
Uses LLM to label processes and clusters for better detection. Don't worry, it consumes very less tokens one time.
</p>
{!hasLLMProvider && (
<p className="text-xs text-amber-400 mt-2">
Setup LLM provider to enable smart clustering
</p>
)}
</div>
<button
type="button"
onClick={() => hasLLMProvider && setEnableSmartClustering(!enableSmartClustering)}
disabled={!hasLLMProvider}
className={`
relative inline-flex h-6 w-11 items-center rounded-full transition-colors ml-4
${enableSmartClustering && hasLLMProvider ? 'bg-accent' : 'bg-gray-700'}
${!hasLLMProvider ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}
`}>
<span
className={`
inline-block h-4 w-4 transform rounded-full bg-white transition-transform
${enableSmartClustering && hasLLMProvider ? 'translate-x-6' : 'translate-x-1'}
`}
/>
</button>
</label>
</div>
</div>
</>
)}
{/* GitHub URL Tab */}
@ -302,11 +351,11 @@ export const DropZone = ({ onFileSelect, onGitClone }: DropZoneProps) => {
{isCloning ? (
<>
<Loader2 className="w-5 h-5 animate-spin" />
{cloneProgress.phase === 'cloning'
{cloneProgress.phase === 'cloning'
? `Cloning... ${cloneProgress.percent}%`
: cloneProgress.phase === 'reading'
? 'Reading files...'
: 'Starting...'
? 'Reading files...'
: 'Starting...'
}
</>
) : (
@ -322,7 +371,7 @@ export const DropZone = ({ onFileSelect, onGitClone }: DropZoneProps) => {
{isCloning && (
<div className="mt-4">
<div className="h-2 bg-elevated rounded-full overflow-hidden">
<div
<div
className="h-full bg-accent transition-all duration-300 ease-out"
style={{ width: `${cloneProgress.percent}%` }}
/>

View file

@ -115,7 +115,23 @@ export const GraphCanvas = forwardRef<GraphCanvasHandle>((_, ref) => {
// Update Sigma graph when KnowledgeGraph changes
useEffect(() => {
if (!graph) return;
const sigmaGraph = knowledgeGraphToGraphology(graph);
// Build communityMemberships map from MEMBER_OF relationships
// MEMBER_OF edges: nodeId -> communityId (stored as targetId)
const communityMemberships = new Map<string, number>();
graph.relationships.forEach(rel => {
if (rel.type === 'MEMBER_OF') {
// Find the community node to get its index
const communityNode = graph.nodes.find(n => n.id === rel.targetId && n.label === 'Community');
if (communityNode) {
// Extract community index from id (e.g., "comm_5" -> 5)
const communityIdx = parseInt(rel.targetId.replace('comm_', ''), 10) || 0;
communityMemberships.set(rel.sourceId, communityIdx);
}
}
});
const sigmaGraph = knowledgeGraphToGraphology(graph, communityMemberships);
setSigmaGraph(sigmaGraph);
}, [graph, setSigmaGraph]);

View file

@ -0,0 +1,140 @@
import { Brain, Sparkles, X, Settings } from 'lucide-react';
interface IntelligentClusteringModalProps {
isOpen: boolean;
onClose: () => void;
onEnable: () => void;
onConfigure: () => void;
}
export const IntelligentClusteringModal = ({
isOpen,
onClose,
onEnable,
onConfigure
}: IntelligentClusteringModalProps) => {
if (!isOpen) return null;
// Parent handles all state updates via onEnable/onConfigure/onClose
const handleEnable = () => {
onEnable();
};
const handleSkip = () => {
onClose();
};
return (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
{/* Backdrop */}
<div
className="absolute inset-0 bg-black/60 backdrop-blur-sm"
onClick={handleSkip}
/>
{/* Modal Content */}
<div className="relative bg-surface border border-border-subtle rounded-2xl shadow-2xl max-w-md w-full overflow-hidden animate-in fade-in zoom-in-95 duration-200">
{/* Header with cool gradient background */}
<div className="bg-gradient-to-br from-accent/20 to-surface p-6 pb-8 border-b border-border-subtle/50 relative overflow-hidden">
<div className="absolute top-0 right-0 p-4 opacity-10">
<Brain className="w-32 h-32 rotate-12" />
</div>
<button
onClick={handleSkip}
className="absolute top-4 right-4 p-2 text-text-muted hover:text-text-primary rounded-full hover:bg-black/10 transition-colors"
>
<X className="w-5 h-5" />
</button>
<div className="flex items-center gap-3 mb-2">
<div className="p-3 bg-accent text-white rounded-xl shadow-lg shadow-accent/20">
<Sparkles className="w-6 h-6" />
</div>
</div>
<h2 className="text-xl font-bold text-text-primary mt-4">
Upgrade to Intelligent Clustering?
</h2>
<p className="text-text-secondary mt-1 text-sm leading-relaxed">
Your clusters are ready, but they could be smarter! Right now they're just named after folders.
</p>
</div>
{/* Body */}
<div className="p-6 space-y-6">
<div className="space-y-3">
<h3 className="text-sm font-semibold text-text-primary flex items-center gap-2">
<Brain className="w-4 h-4 text-accent" />
What you get:
</h3>
<ul className="space-y-2 text-sm text-text-secondary">
<li className="flex items-start gap-2">
<span className="text-green-400 mt-1"></span>
Semantic names (e.g., "Auth System" vs "utils")
</li>
<li className="flex items-start gap-2">
<span className="text-green-400 mt-1"></span>
Search keywords for better agent context
</li>
<li className="flex items-start gap-2">
<span className="text-green-400 mt-1"></span>
Descriptions of what the code actually does
</li>
</ul>
</div>
{/* How it works */}
<div className="p-4 bg-elevated/50 border border-border-subtle rounded-xl">
<div className="flex items-start gap-3">
<div className="p-2 bg-blue-500/10 rounded-lg text-blue-400">
<Brain className="w-4 h-4" />
</div>
<div>
<h4 className="text-sm font-medium text-text-primary">Uses Your Configured LLM</h4>
<p className="text-xs text-text-muted mt-1 leading-relaxed">
Runs on your own API key. Very low token usage (~$0.01 for most codebases).
<br />
<span className="text-amber-400">💡 Tip:</span> Use a cheaper model like GPT-4o-mini in settings!
</p>
</div>
</div>
</div>
</div>
{/* Actions */}
<div className="p-6 pt-2 bg-surface flex flex-col gap-3">
<button
onClick={handleEnable}
className="w-full py-3 px-4 bg-accent text-white font-medium rounded-xl hover:bg-accent-dim shadow-lg shadow-accent/20 transition-all active:scale-[0.98] flex items-center justify-center gap-2"
>
<Sparkles className="w-4 h-4" />
Enable Smart Clustering
</button>
<div className="flex bg-elevated rounded-xl p-1 gap-1">
<button
onClick={onConfigure}
className="flex-1 py-2 px-3 text-sm font-medium text-text-secondary hover:text-text-primary hover:bg-hover rounded-lg transition-colors flex items-center justify-center gap-2"
>
<Settings className="w-3 h-3" />
Configure Model
</button>
<div className="w-px bg-border-subtle my-2" />
<button
onClick={handleSkip}
className="flex-1 py-2 px-3 text-sm font-medium text-text-muted hover:text-text-primary hover:bg-hover rounded-lg transition-colors"
>
No thanks (Free)
</button>
</div>
</div>
</div>
</div>
);
};

View file

@ -1,5 +1,5 @@
import { useState, useEffect, useCallback } from 'react';
import { X, Key, Server, Brain, Check, AlertCircle, Eye, EyeOff, RefreshCw } from 'lucide-react';
import { X, Key, Server, Brain, Check, AlertCircle, Eye, EyeOff, RefreshCw, Sparkles } from 'lucide-react';
import {
loadSettings,
saveSettings,
@ -534,6 +534,147 @@ export const SettingsPanel = ({ isOpen, onClose, onSettingsSaved }: SettingsPane
</div>
)}
{/* Intelligent Clustering Settings */}
<div className="space-y-4 animate-fade-in pt-2 border-t border-border-subtle">
<h3 className="text-sm font-semibold text-text-primary flex items-center gap-2">
<Sparkles className="w-4 h-4 text-accent" />
Intelligent Clustering (Beta)
</h3>
<div className="p-4 bg-elevated/50 border border-border-subtle rounded-xl space-y-4">
<div className="flex items-center justify-between">
<div>
<label className="text-sm font-medium text-text-primary">Enable AI Enrichment</label>
<p className="text-xs text-text-muted">Generate semantic names and descriptions for code clusters</p>
</div>
<button
onClick={() => setSettings(prev => ({ ...prev, intelligentClustering: !prev.intelligentClustering }))}
className={`
relative inline-flex h-6 w-11 items-center rounded-full transition-colors
${settings.intelligentClustering ? 'bg-accent' : 'bg-gray-700'}
`}
>
<span
className={`
inline-block h-4 w-4 transform rounded-full bg-white transition-transform
${settings.intelligentClustering ? 'translate-x-6' : 'translate-x-1'}
`}
/>
</button>
</div>
{settings.intelligentClustering && (
<div className="space-y-4 animate-fade-in pl-1">
<div className="flex items-center justify-between">
<div>
<label className="text-sm font-medium text-text-secondary">Use Agent Model</label>
<p className="text-xs text-text-muted">Use the same provider configured above</p>
</div>
<button
onClick={() => setSettings(prev => ({ ...prev, useSameModelForClustering: !prev.useSameModelForClustering }))}
className={`
relative inline-flex h-6 w-11 items-center rounded-full transition-colors
${settings.useSameModelForClustering ? 'bg-accent/70' : 'bg-gray-700'}
`}
>
<span
className={`
inline-block h-4 w-4 transform rounded-full bg-white transition-transform
${settings.useSameModelForClustering ? 'translate-x-6' : 'translate-x-1'}
`}
/>
</button>
</div>
{!settings.useSameModelForClustering && (
<div className="space-y-3 pt-2 bg-black/20 p-3 rounded-lg">
<div className="flex items-center gap-2 mb-2">
<div className="p-1 bg-amber-500/10 rounded text-amber-400">
<Brain className="w-3 h-3" />
</div>
<p className="text-xs text-amber-300">
<span className="font-semibold">Pro Tip:</span> Use a cheaper model like GPT-4o-mini or Gemini Flash for clustering!
</p>
</div>
{/* Simplistic Clustering Provider Config - For now just a model name override for simplicity,
or we could duplicate the provider selector.
For key simplicity in this iteration, let's just let them override the MODEL name if using the SAME provider,
or we can add a provider dropdown.
Actually, the simplest implementation for "separate model" is just allowing them to pick a provider/model
for clustering specifically. But that replicates a lot of UI.
Let's stick to the plan: "Use same model as agent" vs "Use different model".
If different, show a simplified provider config (just Provider + Model + Key if needed).
For MVP, let's just assume they want to use OpenAI/Azure/Gemini with a specific model string.
*/}
<div className="space-y-2">
<label className="block text-sm font-medium text-text-secondary">
Clustering Provider
</label>
<select
value={settings.clusteringProvider?.provider || settings.activeProvider}
onChange={(e) => setSettings(prev => ({
...prev,
clusteringProvider: {
...prev.clusteringProvider,
provider: e.target.value as any
}
}))}
className="w-full px-3 py-2 bg-elevated border border-border-subtle rounded-lg text-sm text-text-primary outline-none"
>
{providers.map(p => (
<option key={p} value={p}>{getProviderDisplayName(p)}</option>
))}
</select>
</div>
<div className="space-y-2">
<label className="block text-sm font-medium text-text-secondary">
Model Name
</label>
<input
type="text"
value={settings.clusteringProvider?.model || ''}
onChange={e => setSettings(prev => ({
...prev,
clusteringProvider: { ...prev.clusteringProvider, model: e.target.value }
}))}
placeholder="e.g. gpt-4o-mini"
className="w-full px-3 py-2 bg-elevated border border-border-subtle rounded-lg text-sm text-text-primary placeholder:text-text-muted outline-none"
/>
</div>
<div className="space-y-2">
<label className="block text-sm font-medium text-text-secondary">
API Key (Optional)
</label>
<input
type="password"
value={(settings.clusteringProvider as any)?.apiKey || ''}
onChange={e => setSettings(prev => ({
...prev,
clusteringProvider: { ...prev.clusteringProvider, apiKey: e.target.value }
}))}
placeholder="Leave blank to use main key if matching..."
className="w-full px-3 py-2 bg-elevated border border-border-subtle rounded-lg text-sm text-text-primary placeholder:text-text-muted outline-none"
/>
</div>
<p className="text-xs text-text-muted">
Required if using a different provider than your main agent.
</p>
</div>
)}
</div>
)}
</div>
</div>
{/* Privacy Note */}
<div className="p-4 bg-elevated/50 border border-border-subtle rounded-xl">
<div className="flex gap-3">

View file

@ -1,7 +1,8 @@
import { Pause, X } from 'lucide-react';
import { useAppState } from '../hooks/useAppState';
export const StatusBar = () => {
const { graph, progress } = useAppState();
const { graph, progress, enrichmentProgress, cancelEnrichment } = useAppState();
const nodeCount = graph?.nodes.length ?? 0;
const edgeCount = graph?.relationships.length ?? 0;
@ -13,15 +14,17 @@ export const StatusBar = () => {
.map(n => n.properties.language)
.filter(Boolean);
if (languages.length === 0) return null;
const counts = languages.reduce((acc, lang) => {
acc[lang!] = (acc[lang!] || 0) + 1;
return acc;
}, {} as Record<string, number>);
return Object.entries(counts).sort((a, b) => b[1] - a[1])[0]?.[0];
})();
const isLabeling = enrichmentProgress !== null;
return (
<footer className="flex items-center justify-between px-5 py-2 bg-deep border-t border-dashed border-border-subtle text-[11px] text-text-muted">
{/* Left - Status */}
@ -29,13 +32,31 @@ export const StatusBar = () => {
{progress && progress.phase !== 'complete' ? (
<>
<div className="w-28 h-1 bg-elevated rounded-full overflow-hidden">
<div
<div
className="h-full bg-gradient-to-r from-accent to-node-interface rounded-full transition-all duration-300"
style={{ width: `${progress.percent}%` }}
/>
</div>
<span>{progress.message}</span>
</>
) : isLabeling ? (
<>
<div className="w-28 h-1 bg-elevated rounded-full overflow-hidden">
<div
className="h-full bg-gradient-to-r from-node-interface to-accent rounded-full transition-all duration-300"
style={{ width: `${(enrichmentProgress.current / enrichmentProgress.total) * 100}%` }}
/>
</div>
<span>Labeling clusters {enrichmentProgress.current}/{enrichmentProgress.total}...</span>
<button
onClick={() => cancelEnrichment()}
className="flex items-center gap-1 px-2 py-0.5 text-[10px] bg-elevated hover:bg-border-subtle rounded transition-colors"
title="Stop LLM labeling and use heuristic labels"
>
<X size={12} />
Stop
</button>
</>
) : (
<div className="flex items-center gap-1.5">
<span className="w-1.5 h-1.5 bg-node-function rounded-full" />
@ -63,4 +84,3 @@ export const StatusBar = () => {
</footer>
);
};

View file

@ -2,12 +2,12 @@ export enum SupportedLanguages {
JavaScript = 'javascript',
TypeScript = 'typescript',
Python = 'python',
// Java = 'java',
// C = 'c',
// CPlusPlus = 'cpp',
// CSharp = 'csharp',
// Go = 'go',
// Rust = 'rust',
Java = 'java',
C = 'c',
CPlusPlus = 'cpp',
CSharp = 'csharp',
Go = 'go',
Rust = 'rust',
// PHP = 'php',
// Ruby = 'ruby',
// Swift = 'swift',

View file

@ -13,7 +13,8 @@ export type NodeLabel =
| 'Decorator'
| 'Import'
| 'Type'
| 'CodeElement';
| 'CodeElement'
| 'Community';
export type NodeProperties = {
@ -23,6 +24,13 @@ export type NodeProperties = {
endLine?: number,
language?: string,
isExported?: boolean,
// Community-specific properties
heuristicLabel?: string,
cohesion?: number,
symbolCount?: number,
keywords?: string[],
description?: string,
enrichedBy?: 'heuristic' | 'llm',
}
export type RelationshipType =
@ -36,6 +44,7 @@ export type RelationshipType =
| 'DECORATES'
| 'IMPLEMENTS'
| 'EXTENDS'
| 'MEMBER_OF'
export interface GraphNode {
id: string,

View file

@ -23,6 +23,17 @@ const FUNCTION_NODE_TYPES = new Set([
// Common async variants
'async_function_declaration',
'async_arrow_function',
// Java
'method_declaration',
'constructor_declaration',
// C/C++
// 'function_definition' already included above
// Go
// 'method_declaration' already included from Java
// C#
'local_function_statement',
// Rust
'function_item',
]);
/**

View file

@ -0,0 +1,245 @@
/**
* Cluster Enricher
*
* LLM-based enrichment for community clusters.
* Generates semantic names, keywords, and descriptions using an LLM.
*/
import { CommunityNode } from './community-processor';
// ============================================================================
// TYPES
// ============================================================================
export interface ClusterEnrichment {
name: string;
keywords: string[];
description: string;
}
export interface EnrichmentResult {
enrichments: Map<string, ClusterEnrichment>;
tokensUsed: number;
}
export interface LLMClient {
generate: (prompt: string) => Promise<string>;
}
export interface ClusterMemberInfo {
name: string;
filePath: string;
type: string; // 'Function' | 'Class' | 'Method' | 'Interface'
}
// ============================================================================
// PROMPT TEMPLATE
// ============================================================================
const buildEnrichmentPrompt = (
members: ClusterMemberInfo[],
heuristicLabel: string
): string => {
// Limit to first 20 members to control token usage
const limitedMembers = members.slice(0, 20);
const memberList = limitedMembers
.map(m => `${m.name} (${m.type})`)
.join(', ');
return `Analyze this code cluster and provide a semantic name and short description.
Heuristic: "${heuristicLabel}"
Members: ${memberList}${members.length > 20 ? ` (+${members.length - 20} more)` : ''}
Reply with JSON only:
{"name": "2-4 word semantic name", "description": "One sentence describing purpose"}`
};
// ============================================================================
// PARSE LLM RESPONSE
// ============================================================================
const parseEnrichmentResponse = (
response: string,
fallbackLabel: string
): ClusterEnrichment => {
try {
// Extract JSON from response (handles markdown code blocks)
const jsonMatch = response.match(/\{[\s\S]*\}/);
if (!jsonMatch) {
throw new Error('No JSON found in response');
}
const parsed = JSON.parse(jsonMatch[0]);
return {
name: parsed.name || fallbackLabel,
keywords: Array.isArray(parsed.keywords) ? parsed.keywords : [],
description: parsed.description || '',
};
} catch {
// Fallback if parsing fails
return {
name: fallbackLabel,
keywords: [],
description: '',
};
}
};
// ============================================================================
// MAIN ENRICHMENT FUNCTION
// ============================================================================
/**
* Enrich clusters with LLM-generated names, keywords, and descriptions
*
* @param communities - Community nodes to enrich
* @param memberMap - Map of communityId -> member info
* @param llmClient - LLM client for generation
* @param onProgress - Progress callback
*/
export const enrichClusters = async (
communities: CommunityNode[],
memberMap: Map<string, ClusterMemberInfo[]>,
llmClient: LLMClient,
onProgress?: (current: number, total: number) => void
): Promise<EnrichmentResult> => {
const enrichments = new Map<string, ClusterEnrichment>();
let tokensUsed = 0;
for (let i = 0; i < communities.length; i++) {
const community = communities[i];
const members = memberMap.get(community.id) || [];
onProgress?.(i + 1, communities.length);
if (members.length === 0) {
// No members, use heuristic
enrichments.set(community.id, {
name: community.heuristicLabel,
keywords: [],
description: '',
});
continue;
}
try {
const prompt = buildEnrichmentPrompt(members, community.heuristicLabel);
const response = await llmClient.generate(prompt);
// Rough token estimate
tokensUsed += prompt.length / 4 + response.length / 4;
const enrichment = parseEnrichmentResponse(response, community.heuristicLabel);
enrichments.set(community.id, enrichment);
} catch (error) {
// On error, fallback to heuristic
console.warn(`Failed to enrich cluster ${community.id}:`, error);
enrichments.set(community.id, {
name: community.heuristicLabel,
keywords: [],
description: '',
});
}
}
return { enrichments, tokensUsed };
};
// ============================================================================
// BATCH ENRICHMENT (more efficient)
// ============================================================================
/**
* Enrich multiple clusters in a single LLM call (batch mode)
* More efficient for token usage but requires larger context window
*/
export const enrichClustersBatch = async (
communities: CommunityNode[],
memberMap: Map<string, ClusterMemberInfo[]>,
llmClient: LLMClient,
batchSize: number = 5,
onProgress?: (current: number, total: number) => void
): Promise<EnrichmentResult> => {
const enrichments = new Map<string, ClusterEnrichment>();
let tokensUsed = 0;
// Process in batches
for (let i = 0; i < communities.length; i += batchSize) {
// Report progress
onProgress?.(Math.min(i + batchSize, communities.length), communities.length);
const batch = communities.slice(i, i + batchSize);
const batchPrompt = batch.map((community, idx) => {
const members = memberMap.get(community.id) || [];
const limitedMembers = members.slice(0, 15);
const memberList = limitedMembers
.map(m => `${m.name} (${m.type})`)
.join(', ');
return `Cluster ${idx + 1} (id: ${community.id}):
Heuristic: "${community.heuristicLabel}"
Members: ${memberList}`;
}).join('\n\n');
const prompt = `Analyze these code clusters and generate semantic names, keywords, and descriptions.
${batchPrompt}
Output JSON array:
[
{"id": "comm_X", "name": "...", "keywords": [...], "description": "..."},
...
]`;
try {
const response = await llmClient.generate(prompt);
tokensUsed += prompt.length / 4 + response.length / 4;
// Parse batch response
const jsonMatch = response.match(/\[[\s\S]*\]/);
if (jsonMatch) {
const parsed = JSON.parse(jsonMatch[0]) as Array<{
id: string;
name: string;
keywords: string[];
description: string;
}>;
for (const item of parsed) {
enrichments.set(item.id, {
name: item.name,
keywords: item.keywords || [],
description: item.description || '',
});
}
}
} catch (error) {
console.warn('Batch enrichment failed, falling back to heuristics:', error);
// Fallback for this batch
for (const community of batch) {
enrichments.set(community.id, {
name: community.heuristicLabel,
keywords: [],
description: '',
});
}
}
}
// Fill in any missing communities
for (const community of communities) {
if (!enrichments.has(community.id)) {
enrichments.set(community.id, {
name: community.heuristicLabel,
keywords: [],
description: '',
});
}
}
return { enrichments, tokensUsed };
};

View file

@ -0,0 +1,354 @@
/**
* Community Detection Processor
*
* Uses the Leiden algorithm (via graphology-communities-louvain) to detect
* communities/clusters in the code graph based on CALLS relationships.
*
* Communities represent groups of code that work together frequently,
* helping agents navigate the codebase by functional area rather than file structure.
*/
import Graph from 'graphology';
import louvain from 'graphology-communities-louvain';
import { KnowledgeGraph, NodeLabel } from '../graph/types';
// ============================================================================
// TYPES
// ============================================================================
export interface CommunityNode {
id: string;
label: string;
heuristicLabel: string;
cohesion: number;
symbolCount: number;
}
export interface CommunityMembership {
nodeId: string;
communityId: string;
}
export interface CommunityDetectionResult {
communities: CommunityNode[];
memberships: CommunityMembership[];
stats: {
totalCommunities: number;
modularity: number;
nodesProcessed: number;
};
}
// ============================================================================
// COMMUNITY COLORS (for visualization)
// ============================================================================
export const COMMUNITY_COLORS = [
'#ef4444', // red
'#f97316', // orange
'#eab308', // yellow
'#22c55e', // green
'#06b6d4', // cyan
'#3b82f6', // blue
'#8b5cf6', // violet
'#d946ef', // fuchsia
'#ec4899', // pink
'#f43f5e', // rose
'#14b8a6', // teal
'#84cc16', // lime
];
export const getCommunityColor = (communityIndex: number): string => {
return COMMUNITY_COLORS[communityIndex % COMMUNITY_COLORS.length];
};
// ============================================================================
// MAIN PROCESSOR
// ============================================================================
/**
* Detect communities in the knowledge graph using Leiden algorithm
*
* This runs AFTER all relationships (CALLS, IMPORTS, etc.) have been built.
* It uses primarily CALLS edges to cluster code that works together.
*/
export const processCommunities = async (
knowledgeGraph: KnowledgeGraph,
onProgress?: (message: string, progress: number) => void
): Promise<CommunityDetectionResult> => {
onProgress?.('Building graph for community detection...', 0);
// Step 1: Build a graphology graph from the knowledge graph
// We only include symbol nodes (Function, Class, Method) and CALLS edges
const graph = buildGraphologyGraph(knowledgeGraph);
if (graph.order === 0) {
// No nodes to cluster
return {
communities: [],
memberships: [],
stats: { totalCommunities: 0, modularity: 0, nodesProcessed: 0 }
};
}
onProgress?.(`Running Leiden algorithm on ${graph.order} nodes...`, 30);
// Step 2: Run Leiden (via Louvain implementation with refinement)
const details = louvain.detailed(graph, {
resolution: 1.0, // Default resolution, can be tuned
randomWalk: true,
});
onProgress?.(`Found ${details.count} communities...`, 60);
// Step 3: Create community nodes with heuristic labels
const communityNodes = createCommunityNodes(
details.communities as Record<string, number>,
details.count,
graph,
knowledgeGraph
);
onProgress?.('Creating membership edges...', 80);
// Step 4: Create membership mappings
const memberships: CommunityMembership[] = [];
Object.entries(details.communities).forEach(([nodeId, communityNum]) => {
memberships.push({
nodeId,
communityId: `comm_${communityNum}`,
});
});
onProgress?.('Community detection complete!', 100);
return {
communities: communityNodes,
memberships,
stats: {
totalCommunities: details.count,
modularity: details.modularity,
nodesProcessed: graph.order,
}
};
};
// ============================================================================
// HELPER: Build graphology graph from knowledge graph
// ============================================================================
/**
* Build a graphology graph containing only symbol nodes and CALLS edges
* This is what the Leiden algorithm will cluster
*/
const buildGraphologyGraph = (knowledgeGraph: KnowledgeGraph): Graph => {
// Use undirected graph for Leiden - it looks at edge density, not direction
const graph = new Graph({ type: 'undirected', allowSelfLoops: false });
// Symbol types that should be clustered
const symbolTypes = new Set<NodeLabel>(['Function', 'Class', 'Method', 'Interface']);
// Add symbol nodes
knowledgeGraph.nodes.forEach(node => {
if (symbolTypes.has(node.label)) {
graph.addNode(node.id, {
name: node.properties.name,
filePath: node.properties.filePath,
type: node.label,
});
}
});
// Add CALLS edges (primary clustering signal)
// We can also include EXTENDS/IMPLEMENTS for OOP clustering
const clusteringRelTypes = new Set(['CALLS', 'EXTENDS', 'IMPLEMENTS']);
knowledgeGraph.relationships.forEach(rel => {
if (clusteringRelTypes.has(rel.type)) {
// Only add edge if both nodes exist in our symbol graph
// Also skip self-loops (recursive calls) - not allowed in undirected graph
if (graph.hasNode(rel.sourceId) && graph.hasNode(rel.targetId) && rel.sourceId !== rel.targetId) {
// Avoid duplicate edges
if (!graph.hasEdge(rel.sourceId, rel.targetId)) {
graph.addEdge(rel.sourceId, rel.targetId);
}
}
}
});
return graph;
};
// ============================================================================
// HELPER: Create community nodes with heuristic labels
// ============================================================================
/**
* Create Community nodes with auto-generated labels based on member file paths
*/
const createCommunityNodes = (
communities: Record<string, number>,
communityCount: number,
graph: Graph,
knowledgeGraph: KnowledgeGraph
): CommunityNode[] => {
// Group node IDs by community
const communityMembers = new Map<number, string[]>();
Object.entries(communities).forEach(([nodeId, commNum]) => {
if (!communityMembers.has(commNum)) {
communityMembers.set(commNum, []);
}
communityMembers.get(commNum)!.push(nodeId);
});
// Build node lookup for file paths
const nodePathMap = new Map<string, string>();
knowledgeGraph.nodes.forEach(node => {
if (node.properties.filePath) {
nodePathMap.set(node.id, node.properties.filePath);
}
});
// Create community nodes - SKIP SINGLETONS (isolated nodes)
const communityNodes: CommunityNode[] = [];
communityMembers.forEach((memberIds, commNum) => {
// Skip singleton communities - they're just isolated nodes
if (memberIds.length < 2) return;
const heuristicLabel = generateHeuristicLabel(memberIds, nodePathMap, graph, commNum);
communityNodes.push({
id: `comm_${commNum}`,
label: heuristicLabel,
heuristicLabel,
cohesion: calculateCohesion(memberIds, graph),
symbolCount: memberIds.length,
});
});
// Sort by size descending
communityNodes.sort((a, b) => b.symbolCount - a.symbolCount);
return communityNodes;
};
// ============================================================================
// HELPER: Generate heuristic label from folder patterns
// ============================================================================
/**
* Generate a human-readable label from the most common folder name in the community
*/
const generateHeuristicLabel = (
memberIds: string[],
nodePathMap: Map<string, string>,
graph: Graph,
commNum: number
): string => {
// Collect folder names from file paths
const folderCounts = new Map<string, number>();
memberIds.forEach(nodeId => {
const filePath = nodePathMap.get(nodeId) || '';
const parts = filePath.split('/').filter(Boolean);
// Get the most specific folder (parent directory)
if (parts.length >= 2) {
const folder = parts[parts.length - 2];
// Skip generic folder names
if (!['src', 'lib', 'core', 'utils', 'common', 'shared', 'helpers'].includes(folder.toLowerCase())) {
folderCounts.set(folder, (folderCounts.get(folder) || 0) + 1);
}
}
});
// Find most common folder
let maxCount = 0;
let bestFolder = '';
folderCounts.forEach((count, folder) => {
if (count > maxCount) {
maxCount = count;
bestFolder = folder;
}
});
if (bestFolder) {
// Capitalize first letter
return bestFolder.charAt(0).toUpperCase() + bestFolder.slice(1);
}
// Fallback: use function names to detect patterns
const names: string[] = [];
memberIds.forEach(nodeId => {
const name = graph.getNodeAttribute(nodeId, 'name');
if (name) names.push(name);
});
// Look for common prefixes
if (names.length > 2) {
const commonPrefix = findCommonPrefix(names);
if (commonPrefix.length > 2) {
return commonPrefix.charAt(0).toUpperCase() + commonPrefix.slice(1);
}
}
// Last resort: generic name with community ID for uniqueness
return `Cluster_${commNum}`;
};
/**
* Find common prefix among strings
*/
const findCommonPrefix = (strings: string[]): string => {
if (strings.length === 0) return '';
const sorted = strings.slice().sort();
const first = sorted[0];
const last = sorted[sorted.length - 1];
let i = 0;
while (i < first.length && first[i] === last[i]) {
i++;
}
return first.substring(0, i);
};
// ============================================================================
// HELPER: Calculate community cohesion
// ============================================================================
/**
* Calculate cohesion score (0-1) based on internal edge density
* Higher cohesion = more internal connections relative to size
*/
const calculateCohesion = (memberIds: string[], graph: Graph): number => {
if (memberIds.length <= 1) return 1.0;
const memberSet = new Set(memberIds);
let internalEdges = 0;
// Count edges within the community
memberIds.forEach(nodeId => {
if (graph.hasNode(nodeId)) {
graph.forEachNeighbor(nodeId, neighbor => {
if (memberSet.has(neighbor)) {
internalEdges++;
}
});
}
});
// Each edge is counted twice (once from each end), so divide by 2
internalEdges = internalEdges / 2;
// Maximum possible internal edges for n nodes: n*(n-1)/2
const maxPossibleEdges = (memberIds.length * (memberIds.length - 1)) / 2;
if (maxPossibleEdges === 0) return 1.0;
return Math.min(1.0, internalEdges / maxPossibleEdges);
};

View file

@ -117,6 +117,33 @@ export const processHeritage = async (
});
}
}
// IMPLEMENTS (Rust): impl Trait for Struct
if (captureMap['heritage.trait'] && captureMap['heritage.class']) {
const structName = captureMap['heritage.class'].text;
const traitName = captureMap['heritage.trait'].text;
// Resolve struct and trait IDs
const structId = symbolTable.lookupExact(file.path, structName) ||
symbolTable.lookupFuzzy(structName)[0]?.nodeId ||
generateId('Struct', `${file.path}:${structName}`);
const traitId = symbolTable.lookupFuzzy(traitName)[0]?.nodeId ||
generateId('Trait', `${traitName}`);
if (structId && traitId) {
const relId = generateId('IMPLEMENTS', `${structId}->${traitId}`);
graph.addRelationship({
id: relId,
sourceId: structId,
targetId: traitId,
type: 'IMPLEMENTS',
confidence: 1.0,
reason: 'trait-impl',
});
}
}
});
// Cleanup

View file

@ -35,8 +35,24 @@ const resolveImportPath = (
const basePath = currentDir.join('/');
// 3. Try extensions (prioritize .tsx for React projects)
const extensions = ['', '.tsx', '.ts', '.jsx', '.js', '/index.tsx', '/index.ts', '/index.jsx', '/index.js'];
// 3. Try extensions for all supported languages
const extensions = [
'',
// TypeScript/JavaScript
'.tsx', '.ts', '.jsx', '.js', '/index.tsx', '/index.ts', '/index.jsx', '/index.js',
// Python
'.py', '/__init__.py',
// Java
'.java',
// C/C++
'.c', '.h', '.cpp', '.hpp', '.cc', '.cxx', '.hxx', '.hh',
// C#
'.cs',
// Go
'.go',
// Rust
'.rs', '/mod.rs'
];
for (const ext of extensions) {
const candidate = basePath + ext;

View file

@ -81,10 +81,37 @@ export const processParsing = async (
let nodeLabel = 'CodeElement';
// Core types
if (captureMap['definition.function']) nodeLabel = 'Function';
else if (captureMap['definition.class']) nodeLabel = 'Class';
else if (captureMap['definition.interface']) nodeLabel = 'Interface';
else if (captureMap['definition.method']) nodeLabel = 'Method';
// Struct types (C, C++, Go, Rust, C#)
else if (captureMap['definition.struct']) nodeLabel = 'Struct';
// Enum types
else if (captureMap['definition.enum']) nodeLabel = 'Enum';
// Namespace/Module (C++, C#, Rust)
else if (captureMap['definition.namespace']) nodeLabel = 'Namespace';
else if (captureMap['definition.module']) nodeLabel = 'Module';
// Rust-specific
else if (captureMap['definition.trait']) nodeLabel = 'Trait';
else if (captureMap['definition.impl']) nodeLabel = 'Impl';
else if (captureMap['definition.type']) nodeLabel = 'TypeAlias';
else if (captureMap['definition.const']) nodeLabel = 'Const';
else if (captureMap['definition.static']) nodeLabel = 'Static';
// C-specific
else if (captureMap['definition.typedef']) nodeLabel = 'Typedef';
else if (captureMap['definition.macro']) nodeLabel = 'Macro';
else if (captureMap['definition.union']) nodeLabel = 'Union';
// C#-specific
else if (captureMap['definition.property']) nodeLabel = 'Property';
else if (captureMap['definition.record']) nodeLabel = 'Record';
else if (captureMap['definition.delegate']) nodeLabel = 'Delegate';
// Java-specific
else if (captureMap['definition.annotation']) nodeLabel = 'Annotation';
else if (captureMap['definition.constructor']) nodeLabel = 'Constructor';
// C++ template
else if (captureMap['definition.template']) nodeLabel = 'Template';
const nodeId = generateId(nodeLabel, `${file.path}:${nodeName}`);

View file

@ -5,6 +5,7 @@ import { processParsing } from './parsing-processor';
import { processImports, createImportMap } from './import-processor';
import { processCalls } from './call-processor';
import { processHeritage } from './heritage-processor';
import { processCommunities, CommunityDetectionResult } from './community-processor';
import { createSymbolTable } from './symbol-table';
import { createASTCache } from './ast-cache';
import { PipelineProgress, PipelineResult } from '../../types/pipeline';
@ -150,7 +151,7 @@ export const runPipelineFromFiles = async (
});
await processHeritage(graph, files, astCache, symbolTable, (current, total) => {
const heritageProgress = 92 + ((current / total) * 6);
const heritageProgress = 88 + ((current / total) * 4);
onProgress({
phase: 'heritage',
percent: Math.round(heritageProgress),
@ -159,12 +160,62 @@ export const runPipelineFromFiles = async (
});
});
// Phase 7: Community Detection (92-98%)
onProgress({
phase: 'communities',
percent: 92,
message: 'Detecting code communities...',
stats: { filesProcessed: files.length, totalFiles: files.length, nodesCreated: graph.nodeCount },
});
const communityResult = await processCommunities(graph, (message, progress) => {
const communityProgress = 92 + (progress * 0.06);
onProgress({
phase: 'communities',
percent: Math.round(communityProgress),
message,
stats: { filesProcessed: files.length, totalFiles: files.length, nodesCreated: graph.nodeCount },
});
});
// Log community detection results
if (import.meta.env.DEV) {
console.log(`🏘️ Community detection: ${communityResult.stats.totalCommunities} communities found (modularity: ${communityResult.stats.modularity.toFixed(3)})`);
}
// Add community nodes to the graph
communityResult.communities.forEach(comm => {
graph.addNode({
id: comm.id,
label: 'Community' as const,
properties: {
name: comm.label,
filePath: '',
heuristicLabel: comm.heuristicLabel,
cohesion: comm.cohesion,
symbolCount: comm.symbolCount,
}
});
});
// Add MEMBER_OF relationships
communityResult.memberships.forEach(membership => {
graph.addRelationship({
id: `${membership.nodeId}_member_of_${membership.communityId}`,
type: 'MEMBER_OF',
sourceId: membership.nodeId,
targetId: membership.communityId,
confidence: 1.0,
reason: 'leiden-algorithm',
});
});
// Phase 6: Complete (100%)
// Phase 8: Complete (100%)
onProgress({
phase: 'complete',
percent: 100,
message: 'Graph generation complete!',
message: `Graph complete! ${communityResult.stats.totalCommunities} communities detected.`,
stats: {
filesProcessed: files.length,
totalFiles: files.length,
@ -175,7 +226,7 @@ export const runPipelineFromFiles = async (
// Cleanup WASM memory before returning
astCache.clear();
return { graph, fileContents };
return { graph, fileContents, communityResult };
} catch (error) {
cleanup();

View file

@ -148,9 +148,184 @@ export const PYTHON_QUERIES = `
(identifier) @heritage.extends)) @heritage
`;
// Java queries - works with tree-sitter-java
export const JAVA_QUERIES = `
; Classes, Interfaces, Enums, Annotations
(class_declaration name: (identifier) @name) @definition.class
(interface_declaration name: (identifier) @name) @definition.interface
(enum_declaration name: (identifier) @name) @definition.enum
(annotation_type_declaration name: (identifier) @name) @definition.annotation
; Methods & Constructors
(method_declaration name: (identifier) @name) @definition.method
(constructor_declaration name: (identifier) @name) @definition.constructor
; Imports
(import_declaration (scoped_identifier) @import.source) @import
; Calls
(method_invocation name: (identifier) @call.name) @call
(method_invocation object: (_) name: (identifier) @call.name) @call
; Heritage - extends class
(class_declaration name: (identifier) @heritage.class
(superclass (type_identifier) @heritage.extends)) @heritage
; Heritage - implements interfaces
(class_declaration name: (identifier) @heritage.class
(super_interfaces (type_list (type_identifier) @heritage.implements))) @heritage.impl
`;
// C queries - works with tree-sitter-c
export const C_QUERIES = `
; Functions
(function_definition declarator: (function_declarator declarator: (identifier) @name)) @definition.function
(declaration declarator: (function_declarator declarator: (identifier) @name)) @definition.function
; Structs, Unions, Enums, Typedefs
(struct_specifier name: (type_identifier) @name) @definition.struct
(union_specifier name: (type_identifier) @name) @definition.union
(enum_specifier name: (type_identifier) @name) @definition.enum
(type_definition declarator: (type_identifier) @name) @definition.typedef
; Macros
(preproc_function_def name: (identifier) @name) @definition.macro
(preproc_def name: (identifier) @name) @definition.macro
; Includes
(preproc_include path: (_) @import.source) @import
; Calls
(call_expression function: (identifier) @call.name) @call
(call_expression function: (field_expression field: (field_identifier) @call.name)) @call
`;
// Go queries - works with tree-sitter-go
export const GO_QUERIES = `
; Functions & Methods
(function_declaration name: (identifier) @name) @definition.function
(method_declaration name: (field_identifier) @name) @definition.method
; Types
(type_declaration (type_spec name: (type_identifier) @name type: (struct_type))) @definition.struct
(type_declaration (type_spec name: (type_identifier) @name type: (interface_type))) @definition.interface
(type_declaration (type_spec name: (type_identifier) @name)) @definition.type
; Imports
(import_declaration (import_spec path: (interpreted_string_literal) @import.source)) @import
(import_declaration (import_spec_list (import_spec path: (interpreted_string_literal) @import.source))) @import
; Calls
(call_expression function: (identifier) @call.name) @call
(call_expression function: (selector_expression field: (field_identifier) @call.name)) @call
`;
// C++ queries - works with tree-sitter-cpp
export const CPP_QUERIES = `
; Classes, Structs, Namespaces
(class_specifier name: (type_identifier) @name) @definition.class
(struct_specifier name: (type_identifier) @name) @definition.struct
(namespace_definition name: (namespace_identifier) @name) @definition.namespace
(enum_specifier name: (type_identifier) @name) @definition.enum
; Functions & Methods
(function_definition declarator: (function_declarator declarator: (identifier) @name)) @definition.function
(function_definition declarator: (function_declarator declarator: (qualified_identifier name: (identifier) @name))) @definition.method
; Templates
(template_declaration (class_specifier name: (type_identifier) @name)) @definition.template
(template_declaration (function_definition declarator: (function_declarator declarator: (identifier) @name))) @definition.template
; Includes
(preproc_include path: (_) @import.source) @import
; Calls
(call_expression function: (identifier) @call.name) @call
(call_expression function: (field_expression field: (field_identifier) @call.name)) @call
(call_expression function: (qualified_identifier name: (identifier) @call.name)) @call
(call_expression function: (template_function name: (identifier) @call.name)) @call
; Heritage
(class_specifier name: (type_identifier) @heritage.class
(base_class_clause (type_identifier) @heritage.extends)) @heritage
(class_specifier name: (type_identifier) @heritage.class
(base_class_clause (access_specifier) (type_identifier) @heritage.extends)) @heritage
`;
// C# queries - works with tree-sitter-c-sharp
export const CSHARP_QUERIES = `
; Types
(class_declaration name: (identifier) @name) @definition.class
(interface_declaration name: (identifier) @name) @definition.interface
(struct_declaration name: (identifier) @name) @definition.struct
(enum_declaration name: (identifier) @name) @definition.enum
(record_declaration name: (identifier) @name) @definition.record
(delegate_declaration name: (identifier) @name) @definition.delegate
; Namespaces
(namespace_declaration name: (identifier) @name) @definition.namespace
(namespace_declaration name: (qualified_name) @name) @definition.namespace
; Methods & Properties
(method_declaration name: (identifier) @name) @definition.method
(local_function_statement name: (identifier) @name) @definition.function
(constructor_declaration name: (identifier) @name) @definition.constructor
(property_declaration name: (identifier) @name) @definition.property
; Using
(using_directive (qualified_name) @import.source) @import
(using_directive (identifier) @import.source) @import
; Calls
(invocation_expression function: (identifier) @call.name) @call
(invocation_expression function: (member_access_expression name: (identifier) @call.name)) @call
; Heritage
(class_declaration name: (identifier) @heritage.class
(base_list (simple_base_type (identifier) @heritage.extends))) @heritage
(class_declaration name: (identifier) @heritage.class
(base_list (simple_base_type (generic_name (identifier) @heritage.extends)))) @heritage
`;
// Rust queries - works with tree-sitter-rust
export const RUST_QUERIES = `
; Functions & Items
(function_item name: (identifier) @name) @definition.function
(struct_item name: (type_identifier) @name) @definition.struct
(enum_item name: (type_identifier) @name) @definition.enum
(trait_item name: (type_identifier) @name) @definition.trait
(impl_item type: (type_identifier) @name) @definition.impl
(mod_item name: (identifier) @name) @definition.module
; Type aliases, const, static, macros
(type_item name: (type_identifier) @name) @definition.type
(const_item name: (identifier) @name) @definition.const
(static_item name: (identifier) @name) @definition.static
(macro_definition name: (identifier) @name) @definition.macro
; Use statements
(use_declaration argument: (_) @import.source) @import
; Calls
(call_expression function: (identifier) @call.name) @call
(call_expression function: (field_expression field: (field_identifier) @call.name)) @call
(call_expression function: (scoped_identifier name: (identifier) @call.name)) @call
(call_expression function: (generic_function function: (identifier) @call.name)) @call
; Heritage (trait implementation)
(impl_item trait: (type_identifier) @heritage.trait type: (type_identifier) @heritage.class) @heritage
(impl_item trait: (generic_type type: (type_identifier) @heritage.trait) type: (type_identifier) @heritage.class) @heritage
`;
export const LANGUAGE_QUERIES: Record<SupportedLanguages, string> = {
[SupportedLanguages.TypeScript]: TYPESCRIPT_QUERIES,
[SupportedLanguages.JavaScript]: JAVASCRIPT_QUERIES,
[SupportedLanguages.Python]: PYTHON_QUERIES,
[SupportedLanguages.Java]: JAVA_QUERIES,
[SupportedLanguages.C]: C_QUERIES,
[SupportedLanguages.Go]: GO_QUERIES,
[SupportedLanguages.CPlusPlus]: CPP_QUERIES,
[SupportedLanguages.CSharp]: CSHARP_QUERIES,
[SupportedLanguages.Rust]: RUST_QUERIES,
};

View file

@ -4,11 +4,27 @@ import { SupportedLanguages } from '../../config/supported-languages';
* Map file extension to SupportedLanguage enum
*/
export const getLanguageFromFilename = (filename: string): SupportedLanguages | null => {
// TypeScript (including TSX)
if (filename.endsWith('.tsx')) return SupportedLanguages.TypeScript;
if (filename.endsWith('.ts')) return SupportedLanguages.TypeScript;
// JavaScript (including JSX)
if (filename.endsWith('.jsx')) return SupportedLanguages.JavaScript;
if (filename.endsWith('.js')) return SupportedLanguages.JavaScript;
// Python
if (filename.endsWith('.py')) return SupportedLanguages.Python;
// Java
if (filename.endsWith('.java')) return SupportedLanguages.Java;
// C (source and headers)
if (filename.endsWith('.c') || filename.endsWith('.h')) return SupportedLanguages.C;
// C++ (all common extensions)
if (filename.endsWith('.cpp') || filename.endsWith('.cc') || filename.endsWith('.cxx') ||
filename.endsWith('.hpp') || filename.endsWith('.hxx') || filename.endsWith('.hh')) return SupportedLanguages.CPlusPlus;
// C#
if (filename.endsWith('.cs')) return SupportedLanguages.CSharp;
// Go
if (filename.endsWith('.go')) return SupportedLanguages.Go;
// Rust
if (filename.endsWith('.rs')) return SupportedLanguages.Rust;
return null;
};

View file

@ -196,6 +196,36 @@ const generateCodeElementCSV = (
return rows.join('\n');
};
/**
* Generate CSV for Community nodes (from Leiden algorithm)
* Headers: id,label,heuristicLabel,keywords,description,enrichedBy,cohesion,symbolCount
*/
const generateCommunityCSV = (nodes: GraphNode[]): string => {
const headers = ['id', 'label', 'heuristicLabel', 'keywords', 'description', 'enrichedBy', 'cohesion', 'symbolCount'];
const rows: string[] = [headers.join(',')];
for (const node of nodes) {
if (node.label !== 'Community') continue;
// Handle keywords array - convert to KuzuDB array format
const keywords = (node.properties as any).keywords || [];
const keywordsStr = `[${keywords.map((k: string) => `'${k.replace(/'/g, "''")}'`).join(',')}]`;
rows.push([
escapeCSVField(node.id),
escapeCSVField(node.properties.name || ''), // label is stored in name
escapeCSVField(node.properties.heuristicLabel || ''),
keywordsStr, // Array format for KuzuDB
escapeCSVField((node.properties as any).description || ''),
escapeCSVField((node.properties as any).enrichedBy || 'heuristic'),
escapeCSVNumber(node.properties.cohesion, 0),
escapeCSVNumber(node.properties.symbolCount, 0),
].join(','));
}
return rows.join('\n');
};
// ============================================================================
// RELATIONSHIP CSV GENERATOR (Single Table)
// ============================================================================
@ -247,6 +277,7 @@ export const generateAllCSVs = (
nodeCSVs.set('Interface', generateCodeElementCSV(nodes, 'Interface', fileContents));
nodeCSVs.set('Method', generateCodeElementCSV(nodes, 'Method', fileContents));
nodeCSVs.set('CodeElement', generateCodeElementCSV(nodes, 'CodeElement', fileContents));
nodeCSVs.set('Community', generateCommunityCSV(nodes));
// Generate single relation CSV
const relCSV = generateRelationCSV(graph);

View file

@ -127,29 +127,51 @@ export const loadGraphToKuzu = async (
const [, fromId, toId, relType, confidenceStr, reason] = match;
const confidence = parseFloat(confidenceStr) || 1.0;
// Extract labels from node IDs (format: Label:path:name)
const fromLabel = fromId.split(':')[0];
const toLabel = toId.split(':')[0];
// Extract labels from node IDs
// Community nodes have IDs like "comm_14" (no colon)
// Other nodes have IDs like "Label:path:name"
const getNodeLabel = (nodeId: string): string => {
if (nodeId.startsWith('comm_')) {
return 'Community';
}
return nodeId.split(':')[0];
};
// Reserved Cypher keywords need backtick escaping
const RESERVED_LABELS = ['Macro', 'Enum', 'Union', 'Const', 'Module', 'Struct'];
const escapeLabel = (label: string): string => {
return RESERVED_LABELS.includes(label) ? `\`${label}\`` : label;
};
const fromLabel = escapeLabel(getNodeLabel(fromId));
const toLabel = escapeLabel(getNodeLabel(toId));
// INSERT with explicit node matching (including confidence and reason)
const insertQuery = `
MATCH (a:${fromLabel} {id: '${fromId.replace(/'/g, "''")}'})
MATCH (b:${toLabel} {id: '${toId.replace(/'/g, "''")}'})
MATCH (a:${fromLabel} {id: '${fromId.replace(/'/g, "''")}'}),
(b:${toLabel} {id: '${toId.replace(/'/g, "''")}'})
CREATE (a)-[:${REL_TABLE_NAME} {type: '${relType}', confidence: ${confidence}, reason: '${reason.replace(/'/g, "''")}'}]->(b)
`;
await conn.query(insertQuery);
insertedRels++;
} catch {
} catch (err) {
// Skip failed insertions (nodes might not exist, or relation pair not allowed by schema)
skippedRels++;
if (import.meta.env.DEV) {
const match = line.match(/"([^"]*)","([^"]*)","([^"]*)",([0-9.]+),"([^"]*)"/);
if (match) {
const [, fromId, toId, relType] = match;
const fromLabel = fromId.split(':')[0];
const toLabel = toId.split(':')[0];
const key = `${relType}:${fromLabel}->${toLabel}`;
skippedRelStats.set(key, (skippedRelStats.get(key) || 0) + 1);
const match = line.match(/"([^"]*)","([^"]*)","([^"]*)",([0-9.]+),"([^"]*)"/);
if (match) {
const [, fromId, toId, relType] = match;
const getNodeLabel = (nodeId: string): string => {
if (nodeId.startsWith('comm_')) return 'Community';
return nodeId.split(':')[0];
};
const fromLabel = getNodeLabel(fromId);
const toLabel = getNodeLabel(toId);
const key = `${relType}:${fromLabel}->` + toLabel;
skippedRelStats.set(key, (skippedRelStats.get(key) || 0) + 1);
// Log each skipped relation
if (import.meta.env.DEV) {
console.warn(`⚠️ Skipped: ${key} | "${fromId}" → "${toId}" | ${err instanceof Error ? err.message : String(err)}`);
}
}
}
@ -204,6 +226,9 @@ const getCopyQuery = (table: NodeTableName, path: string): string => {
if (table === 'Folder') {
return `COPY Folder(id, name, filePath) FROM "${path}" (HEADER=true, PARALLEL=false)`;
}
if (table === 'Community') {
return `COPY Community(id, label, heuristicLabel, keywords, description, enrichedBy, cohesion, symbolCount) FROM "${path}" (HEADER=true, PARALLEL=false)`;
}
// All code element tables: Function, Class, Interface, Method, CodeElement
return `COPY ${table}(id, name, filePath, startLine, endLine, content) FROM "${path}" (HEADER=true, PARALLEL=false)`;
};

View file

@ -12,7 +12,12 @@
// ============================================================================
// NODE TABLE NAMES
// ============================================================================
export const NODE_TABLES = ['File', 'Folder', 'Function', 'Class', 'Interface', 'Method', 'CodeElement'] as const;
export const NODE_TABLES = [
'File', 'Folder', 'Function', 'Class', 'Interface', 'Method', 'CodeElement', 'Community',
// Multi-language support
'Struct', 'Enum', 'Macro', 'Typedef', 'Union', 'Namespace', 'Trait', 'Impl',
'TypeAlias', 'Const', 'Static', 'Property', 'Record', 'Delegate', 'Annotation', 'Constructor', 'Template', 'Module'
] as const;
export type NodeTableName = typeof NODE_TABLES[number];
// ============================================================================
@ -21,7 +26,7 @@ export type NodeTableName = typeof NODE_TABLES[number];
export const REL_TABLE_NAME = 'CodeRelation';
// Valid relation types
export const REL_TYPES = ['CONTAINS', 'DEFINES', 'IMPORTS', 'CALLS', 'EXTENDS', 'IMPLEMENTS'] as const;
export const REL_TYPES = ['CONTAINS', 'DEFINES', 'IMPORTS', 'CALLS', 'EXTENDS', 'IMPLEMENTS', 'MEMBER_OF'] as const;
export type RelType = typeof REL_TYPES[number];
// ============================================================================
@ -105,6 +110,58 @@ CREATE NODE TABLE CodeElement (
PRIMARY KEY (id)
)`;
// ============================================================================
// COMMUNITY NODE TABLE (for Leiden algorithm clusters)
// ============================================================================
export const COMMUNITY_SCHEMA = `
CREATE NODE TABLE Community (
id STRING,
label STRING,
heuristicLabel STRING,
keywords STRING[],
description STRING,
enrichedBy STRING,
cohesion DOUBLE,
symbolCount INT32,
PRIMARY KEY (id)
)`;
// ============================================================================
// MULTI-LANGUAGE NODE TABLE SCHEMAS
// ============================================================================
// Generic code element with startLine/endLine for C, C++, Rust, Go, Java, C#
const CODE_ELEMENT_BASE = (name: string) => `
CREATE NODE TABLE \`${name}\` (
id STRING,
name STRING,
filePath STRING,
startLine INT64,
endLine INT64,
content STRING,
PRIMARY KEY (id)
)`;
export const STRUCT_SCHEMA = CODE_ELEMENT_BASE('Struct');
export const ENUM_SCHEMA = CODE_ELEMENT_BASE('Enum');
export const MACRO_SCHEMA = CODE_ELEMENT_BASE('Macro');
export const TYPEDEF_SCHEMA = CODE_ELEMENT_BASE('Typedef');
export const UNION_SCHEMA = CODE_ELEMENT_BASE('Union');
export const NAMESPACE_SCHEMA = CODE_ELEMENT_BASE('Namespace');
export const TRAIT_SCHEMA = CODE_ELEMENT_BASE('Trait');
export const IMPL_SCHEMA = CODE_ELEMENT_BASE('Impl');
export const TYPE_ALIAS_SCHEMA = CODE_ELEMENT_BASE('TypeAlias');
export const CONST_SCHEMA = CODE_ELEMENT_BASE('Const');
export const STATIC_SCHEMA = CODE_ELEMENT_BASE('Static');
export const PROPERTY_SCHEMA = CODE_ELEMENT_BASE('Property');
export const RECORD_SCHEMA = CODE_ELEMENT_BASE('Record');
export const DELEGATE_SCHEMA = CODE_ELEMENT_BASE('Delegate');
export const ANNOTATION_SCHEMA = CODE_ELEMENT_BASE('Annotation');
export const CONSTRUCTOR_SCHEMA = CODE_ELEMENT_BASE('Constructor');
export const TEMPLATE_SCHEMA = CODE_ELEMENT_BASE('Template');
export const MODULE_SCHEMA = CODE_ELEMENT_BASE('Module');
// ============================================================================
// RELATION TABLE SCHEMA
// Single table with 'type' property - connects all node tables
@ -119,18 +176,79 @@ CREATE REL TABLE ${REL_TABLE_NAME} (
FROM File TO Interface,
FROM File TO Method,
FROM File TO CodeElement,
FROM File TO \`Struct\`,
FROM File TO \`Enum\`,
FROM File TO \`Macro\`,
FROM File TO Typedef,
FROM File TO \`Union\`,
FROM File TO Namespace,
FROM File TO Trait,
FROM File TO Impl,
FROM File TO TypeAlias,
FROM File TO \`Const\`,
FROM File TO Static,
FROM File TO Property,
FROM File TO Record,
FROM File TO Delegate,
FROM File TO Annotation,
FROM File TO Constructor,
FROM File TO Template,
FROM File TO \`Module\`,
FROM Folder TO Folder,
FROM Folder TO File,
FROM Function TO Function,
FROM Function TO Method,
FROM Function TO Class,
FROM Function TO Community,
FROM Function TO \`Macro\`,
FROM Function TO \`Struct\`,
FROM Function TO Template,
FROM Function TO \`Enum\`,
FROM Function TO Namespace,
FROM Function TO TypeAlias,
FROM Class TO Method,
FROM Class TO Function,
FROM Class TO Class,
FROM Class TO Interface,
FROM Class TO Community,
FROM Class TO Template,
FROM Method TO Function,
FROM Method TO Method,
FROM Method TO Class,
FROM Method TO Community,
FROM Method TO Template,
FROM Method TO \`Struct\`,
FROM Template TO Template,
FROM Template TO Function,
FROM Template TO Method,
FROM Template TO Class,
FROM Template TO \`Struct\`,
FROM CodeElement TO Community,
FROM Interface TO Community,
FROM \`Struct\` TO Community,
FROM \`Struct\` TO Trait,
FROM \`Struct\` TO Function,
FROM \`Struct\` TO Method,
FROM \`Enum\` TO Community,
FROM \`Macro\` TO Community,
FROM Typedef TO Community,
FROM \`Union\` TO Community,
FROM Namespace TO Community,
FROM Trait TO Community,
FROM Impl TO Community,
FROM Impl TO Trait,
FROM TypeAlias TO Community,
FROM \`Const\` TO Community,
FROM Static TO Community,
FROM Property TO Community,
FROM Record TO Community,
FROM Delegate TO Community,
FROM Annotation TO Community,
FROM Constructor TO Community,
FROM Constructor TO Interface,
FROM Constructor TO Class,
FROM Template TO Community,
FROM \`Module\` TO Community,
type STRING,
confidence DOUBLE,
reason STRING
@ -169,6 +287,26 @@ export const NODE_SCHEMA_QUERIES = [
INTERFACE_SCHEMA,
METHOD_SCHEMA,
CODE_ELEMENT_SCHEMA,
COMMUNITY_SCHEMA,
// Multi-language support
STRUCT_SCHEMA,
ENUM_SCHEMA,
MACRO_SCHEMA,
TYPEDEF_SCHEMA,
UNION_SCHEMA,
NAMESPACE_SCHEMA,
TRAIT_SCHEMA,
IMPL_SCHEMA,
TYPE_ALIAS_SCHEMA,
CONST_SCHEMA,
STATIC_SCHEMA,
PROPERTY_SCHEMA,
RECORD_SCHEMA,
DELEGATE_SCHEMA,
ANNOTATION_SCHEMA,
CONSTRUCTOR_SCHEMA,
TEMPLATE_SCHEMA,
MODULE_SCHEMA,
];
export const REL_SCHEMA_QUERIES = [

View file

@ -87,6 +87,12 @@ export interface LLMSettings {
gemini?: Partial<Omit<GeminiConfig, 'provider'>>;
anthropic?: Partial<Omit<AnthropicConfig, 'provider'>>;
ollama?: Partial<Omit<OllamaConfig, 'provider'>>;
// Intelligent Clustering Settings
intelligentClustering: boolean;
hasSeenClusteringPrompt: boolean;
useSameModelForClustering: boolean;
clusteringProvider?: Partial<ProviderConfig>; // Optional specific config for clustering
}
/**
@ -94,6 +100,9 @@ export interface LLMSettings {
*/
export const DEFAULT_LLM_SETTINGS: LLMSettings = {
activeProvider: 'gemini',
intelligentClustering: false,
hasSeenClusteringPrompt: false,
useSameModelForClustering: true,
openai: {
apiKey: '',
model: 'gpt-4o',

View file

@ -33,6 +33,12 @@ const getWasmPath = (language: SupportedLanguages, filePath?: string): string =>
[SupportedLanguages.JavaScript]: '/wasm/javascript/tree-sitter-javascript.wasm',
[SupportedLanguages.TypeScript]: '/wasm/typescript/tree-sitter-typescript.wasm',
[SupportedLanguages.Python]: '/wasm/python/tree-sitter-python.wasm',
[SupportedLanguages.Java]: '/wasm/java/tree-sitter-java.wasm',
[SupportedLanguages.C]: '/wasm/c/tree-sitter-c.wasm',
[SupportedLanguages.CPlusPlus]: '/wasm/cpp/tree-sitter-cpp.wasm',
[SupportedLanguages.CSharp]: '/wasm/csharp/tree-sitter-csharp.wasm',
[SupportedLanguages.Go]: '/wasm/go/tree-sitter-go.wasm',
[SupportedLanguages.Rust]: '/wasm/rust/tree-sitter-rust.wasm',
};
return languageFileMap[language];
@ -40,18 +46,27 @@ const getWasmPath = (language: SupportedLanguages, filePath?: string): string =>
export const loadLanguage = async (language: SupportedLanguages, filePath?: string): Promise<void> => {
if (!parser) await loadParser();
const wasmPath = getWasmPath(language, filePath);
// Use wasmPath as cache key to differentiate ts vs tsx
if (languageCache.has(wasmPath)) {
parser!.setLanguage(languageCache.get(wasmPath)!);
return;
}
if (!wasmPath) throw new Error(`Unsupported language: ${language}`);
if (!wasmPath) {
console.error(`❌ [Parser] No WASM path configured for language: ${language}`);
throw new Error(`Unsupported language: ${language}`);
}
const loadedLanguage = await Parser.Language.load(wasmPath);
languageCache.set(wasmPath, loadedLanguage);
parser!.setLanguage(loadedLanguage);
try {
const loadedLanguage = await Parser.Language.load(wasmPath);
languageCache.set(wasmPath, loadedLanguage);
parser!.setLanguage(loadedLanguage);
} catch (error: unknown) {
const errorMessage = error instanceof Error ? error.message : String(error);
console.error(`❌ [Parser] Failed to load WASM grammar for ${language}`);
console.error(` WASM Path: ${wasmPath}`);
console.error(` Error: ${errorMessage}`);
throw new Error(`Failed to load grammar for ${language}: ${errorMessage}`);
}
}

View file

@ -8,7 +8,7 @@ import type { IngestionWorkerApi } from '../workers/ingestion.worker';
import type { FileEntry } from '../services/zip';
import type { EmbeddingProgress, SemanticSearchResult } from '../core/embeddings/types';
import type { LLMSettings, ProviderConfig, AgentStreamChunk, ChatMessage, ToolCallInfo, MessageStep } from '../core/llm/types';
import { loadSettings, getActiveProviderConfig } from '../core/llm/settings-service';
import { loadSettings, getActiveProviderConfig, saveSettings } from '../core/llm/settings-service';
import type { AgentMessage } from '../core/llm/agent';
import { DEFAULT_VISIBLE_EDGES, type EdgeType } from '../lib/constants';
@ -112,8 +112,8 @@ interface AppState {
setProjectName: (name: string) => void;
// Worker API (shared across app)
runPipeline: (file: File, onProgress: (p: PipelineProgress) => void) => Promise<PipelineResult>;
runPipelineFromFiles: (files: FileEntry[], onProgress: (p: PipelineProgress) => void) => Promise<PipelineResult>;
runPipeline: (file: File, onProgress: (p: PipelineProgress) => void, clusteringConfig?: ProviderConfig) => Promise<PipelineResult>;
runPipelineFromFiles: (files: FileEntry[], onProgress: (p: PipelineProgress) => void, clusteringConfig?: ProviderConfig) => Promise<PipelineResult>;
runQuery: (cypher: string) => Promise<any[]>;
isDatabaseReady: () => Promise<boolean>;
@ -123,6 +123,9 @@ interface AppState {
// Embedding methods
startEmbeddings: (forceDevice?: 'webgpu' | 'wasm') => Promise<void>;
startBackgroundEnrichment: () => Promise<void>;
cancelEnrichment: () => Promise<void>;
enrichmentProgress: { current: number; total: number } | null;
semanticSearch: (query: string, k?: number) => Promise<SemanticSearchResult[]>;
semanticSearchWithContext: (query: string, k?: number, hops?: number) => Promise<any[]>;
isEmbeddingReady: boolean;
@ -132,6 +135,7 @@ interface AppState {
// LLM/Agent state
llmSettings: LLMSettings;
updateLLMSettings: (updates: Partial<LLMSettings>) => void;
isSettingsPanelOpen: boolean;
setSettingsPanelOpen: (open: boolean) => void;
isAgentReady: boolean;
@ -145,6 +149,7 @@ interface AppState {
// LLM methods
refreshLLMSettings: () => void;
runClusterEnrichment: () => Promise<void>;
initializeAgent: (overrideProjectName?: string) => Promise<void>;
sendChatMessage: (message: string) => Promise<void>;
clearChat: () => void;
@ -289,6 +294,10 @@ export const AppStateProvider = ({ children }: { children: ReactNode }) => {
const [isCodePanelOpen, setCodePanelOpen] = useState(false);
const [codeReferenceFocus, setCodeReferenceFocus] = useState<CodeReferenceFocus | null>(null);
// Cluster enrichment state
const [enrichmentProgress, setEnrichmentProgress] = useState<{ current: number; total: number } | null>(null);
const enrichmentCancelledRef = useRef(false);
const normalizePath = useCallback((p: string) => {
return p.replace(/\\/g, '/').replace(/^\.?\//, '');
}, []);
@ -427,25 +436,27 @@ export const AppStateProvider = ({ children }: { children: ReactNode }) => {
const runPipeline = useCallback(async (
file: File,
onProgress: (progress: PipelineProgress) => void
onProgress: (progress: PipelineProgress) => void,
clusteringConfig?: ProviderConfig
): Promise<PipelineResult> => {
const api = apiRef.current;
if (!api) throw new Error('Worker not initialized');
const proxiedOnProgress = Comlink.proxy(onProgress);
const serializedResult = await api.runPipeline(file, proxiedOnProgress);
const serializedResult = await api.runPipeline(file, proxiedOnProgress, clusteringConfig);
return deserializePipelineResult(serializedResult, createKnowledgeGraph);
}, []);
const runPipelineFromFiles = useCallback(async (
files: FileEntry[],
onProgress: (progress: PipelineProgress) => void
onProgress: (progress: PipelineProgress) => void,
clusteringConfig?: ProviderConfig
): Promise<PipelineResult> => {
const api = apiRef.current;
if (!api) throw new Error('Worker not initialized');
const proxiedOnProgress = Comlink.proxy(onProgress);
const serializedResult = await api.runPipelineFromFiles(files, proxiedOnProgress);
const serializedResult = await api.runPipelineFromFiles(files, proxiedOnProgress, clusteringConfig);
return deserializePipelineResult(serializedResult, createKnowledgeGraph);
}, []);
@ -510,6 +521,63 @@ export const AppStateProvider = ({ children }: { children: ReactNode }) => {
}
}, []);
// Background cluster enrichment
const startBackgroundEnrichment = useCallback(async (): Promise<void> => {
const api = apiRef.current;
if (!api) return;
enrichmentCancelledRef.current = false;
try {
const result = await api.startBackgroundEnrichment(
Comlink.proxy((current: number, total: number) => {
setEnrichmentProgress({ current, total });
setProgress({
phase: 'complete',
percent: 100,
message: `Labeling clusters ${current}/${total}...`,
});
})
);
setEnrichmentProgress(null);
if (!result.skipped && result.enriched > 0) {
setProgress({
phase: 'complete',
percent: 100,
message: 'Smart cluster labels generated!',
});
// Clear after 3 seconds
setTimeout(() => setProgress(null), 3000);
}
} catch (err) {
console.warn('Background enrichment failed:', err);
setEnrichmentProgress(null);
}
}, []);
// Cancel/pause enrichment
const cancelEnrichment = useCallback(async (): Promise<void> => {
const api = apiRef.current;
if (!api) return;
enrichmentCancelledRef.current = true;
setEnrichmentProgress(null);
try {
await api.cancelEnrichment();
setProgress({
phase: 'complete',
percent: 100,
message: 'LLM labeling stopped. Using heuristic labels.',
});
setTimeout(() => setProgress(null), 3000);
} catch (err) {
console.warn('Cancel enrichment failed:', err);
}
}, []);
const semanticSearch = useCallback(async (
query: string,
k: number = 10
@ -536,6 +604,101 @@ export const AppStateProvider = ({ children }: { children: ReactNode }) => {
}, []);
// LLM methods
const updateLLMSettings = useCallback((updates: Partial<LLMSettings>) => {
setLLMSettings(prev => {
const next = { ...prev, ...updates };
saveSettings(next);
return next;
});
}, []);
const runClusterEnrichment = useCallback(async () => {
const api = apiRef.current;
if (!api) {
setAgentError('Worker not initialized');
return;
}
const defaultConfig = getActiveProviderConfig();
const configToUse = (!llmSettings.useSameModelForClustering && llmSettings.clusteringProvider?.provider)
? {
...defaultConfig,
provider: llmSettings.clusteringProvider.provider || defaultConfig!.provider,
model: llmSettings.clusteringProvider.model || defaultConfig!.model,
apiKey: (llmSettings.clusteringProvider as any).apiKey || (defaultConfig as any).apiKey
} as ProviderConfig
: defaultConfig;
if (!configToUse) {
// No provider configured - open settings panel
setSettingsPanelOpen(true);
setAgentError('Please configure an LLM provider in Settings first.');
return;
}
try {
setProgress({
phase: 'enriching',
percent: 1,
message: 'Starting AI enrichment...',
stats: { filesProcessed: 0, totalFiles: 0, nodesCreated: 0 }
});
const { enrichments } = await api.enrichCommunities(
configToUse,
Comlink.proxy((current, total) => {
setProgress(prev => prev ? ({
...prev,
percent: Math.min(99, 5 + Math.round((current / total) * 90)),
message: `Enriching clusters ${current}/${total}`
}) : null);
})
);
// Update local graph
setGraph(prevGraph => {
if (!prevGraph) return null;
const newNodes = prevGraph.nodes.map(n => {
if (n.label === 'Community' && enrichments[n.id]) {
const e = enrichments[n.id];
return {
...n,
properties: {
...n.properties,
name: e.name,
keywords: e.keywords,
description: e.description,
enrichedBy: 'llm' as const
}
};
}
return n;
});
return { ...prevGraph, nodes: newNodes };
});
setProgress({
phase: 'complete',
percent: 100,
message: '✨ Smart labels generated!',
stats: { filesProcessed: 0, totalFiles: 0, nodesCreated: 0 }
});
// Clear progress after 3 seconds
setTimeout(() => setProgress(null), 3000);
} catch (err) {
console.error(err);
const errorMsg = err instanceof Error ? err.message : String(err);
setAgentError('Clustering enrichment failed: ' + errorMsg);
setProgress(null);
}
}, [llmSettings]);
const refreshLLMSettings = useCallback(() => {
setLLMSettings(loadSettings());
}, []);
@ -1039,6 +1202,9 @@ export const AppStateProvider = ({ children }: { children: ReactNode }) => {
embeddingStatus,
embeddingProgress,
startEmbeddings,
startBackgroundEnrichment,
cancelEnrichment,
enrichmentProgress,
semanticSearch,
semanticSearchWithContext,
isEmbeddingReady: embeddingStatus === 'ready',
@ -1046,6 +1212,7 @@ export const AppStateProvider = ({ children }: { children: ReactNode }) => {
testArrayParams,
// LLM/Agent state
llmSettings,
updateLLMSettings,
isSettingsPanelOpen,
setSettingsPanelOpen,
isAgentReady,
@ -1057,6 +1224,7 @@ export const AppStateProvider = ({ children }: { children: ReactNode }) => {
currentToolCalls,
// LLM methods
refreshLLMSettings,
runClusterEnrichment,
initializeAgent,
sendChatMessage,
clearChat,

View file

@ -0,0 +1,10 @@
import { useAppState } from './useAppState';
export const useSettings = () => {
const { llmSettings, updateLLMSettings } = useAppState();
return {
settings: llmSettings,
updateSettings: updateLLMSettings
};
};

View file

@ -17,6 +17,7 @@ export const NODE_COLORS: Record<NodeLabel, string> = {
Import: '#475569', // Slate darker - very muted
Type: '#a78bfa', // Violet light
CodeElement: '#64748b', // Slate - muted
Community: '#818cf8', // Indigo light - cluster indicator
};
// Node sizes by type - clear visual hierarchy with dramatic size differences
@ -37,6 +38,27 @@ export const NODE_SIZES: Record<NodeLabel, number> = {
Import: 1.5, // Very small - usually hidden anyway
Type: 3, // Type alias - small
CodeElement: 2, // Generic small
Community: 0, // Hidden by default - metadata node
};
// Community color palette for cluster-based coloring
export const COMMUNITY_COLORS = [
'#ef4444', // red
'#f97316', // orange
'#eab308', // yellow
'#22c55e', // green
'#06b6d4', // cyan
'#3b82f6', // blue
'#8b5cf6', // violet
'#d946ef', // fuchsia
'#ec4899', // pink
'#f43f5e', // rose
'#14b8a6', // teal
'#84cc16', // lime
];
export const getCommunityColor = (communityIndex: number): string => {
return COMMUNITY_COLORS[communityIndex % COMMUNITY_COLORS.length];
};
// Labels to show by default (hide imports and variables by default as they clutter)

View file

@ -1,6 +1,6 @@
import Graph from 'graphology';
import { KnowledgeGraph, NodeLabel } from '../core/graph/types';
import { NODE_COLORS, NODE_SIZES } from './constants';
import { NODE_COLORS, NODE_SIZES, getCommunityColor } from './constants';
export interface SigmaNodeAttributes {
x: number;
@ -16,6 +16,8 @@ export interface SigmaNodeAttributes {
zIndex?: number;
highlighted?: boolean;
mass?: number; // ForceAtlas2 mass - higher = more repulsion
community?: number; // Community index from Leiden algorithm
communityColor?: string; // Color assigned by community
}
export interface SigmaEdgeAttributes {
@ -74,9 +76,13 @@ const getNodeMass = (nodeType: NodeLabel, nodeCount: number): number => {
/**
* Converts the KnowledgeGraph to a graphology Graph for Sigma.js
* Folders are positioned in a wide spread, children positioned NEAR their parents
*
* @param knowledgeGraph - The knowledge graph to convert
* @param communityMemberships - Optional map of nodeId -> communityIndex for community coloring
*/
export const knowledgeGraphToGraphology = (
knowledgeGraph: KnowledgeGraph
knowledgeGraph: KnowledgeGraph,
communityMemberships?: Map<string, number>
): Graph<SigmaNodeAttributes, SigmaEdgeAttributes> => {
const graph = new Graph<SigmaNodeAttributes, SigmaEdgeAttributes>();
const nodeCount = knowledgeGraph.nodes.length;
@ -116,6 +122,31 @@ export const knowledgeGraphToGraphology = (
// Small jitter for children around their parent
const childJitter = Math.sqrt(nodeCount) * 3;
// === CLUSTER-BASED POSITIONING ===
// Calculate cluster centers - each cluster gets a region of the graph
const clusterCenters = new Map<number, { x: number; y: number }>();
if (communityMemberships && communityMemberships.size > 0) {
// Find unique community IDs
const communities = new Set(communityMemberships.values());
const communityCount = communities.size;
const clusterSpread = structuralSpread * 0.8; // Clusters spread across 80% of graph
// Position cluster centers using golden angle for even distribution
const goldenAngle = Math.PI * (3 - Math.sqrt(5));
let idx = 0;
communities.forEach(communityId => {
const angle = idx * goldenAngle;
const radius = clusterSpread * Math.sqrt((idx + 1) / communityCount);
clusterCenters.set(communityId, {
x: radius * Math.cos(angle),
y: radius * Math.sin(angle),
});
idx++;
});
}
// Jitter within cluster (tighter than childJitter)
const clusterJitter = Math.sqrt(nodeCount) * 1.5;
// Store positions for parent lookup
const nodePositions = new Map<string, { x: number; y: number }>();
@ -136,6 +167,7 @@ export const knowledgeGraphToGraphology = (
const baseSize = NODE_SIZES[node.label] || 8;
const scaledSize = getScaledNodeSize(baseSize, nodeCount);
// Structural nodes keep their type-based color
graph.addNode(node.id, {
x,
y,
@ -161,18 +193,28 @@ export const knowledgeGraphToGraphology = (
let x: number, y: number;
// Find parent position (parent should already be positioned!)
const parentId = childToParent.get(nodeId);
const parentPos = parentId ? nodePositions.get(parentId) : null;
// Check if this is a symbol node with a community assignment
const communityIndex = communityMemberships?.get(nodeId);
const symbolTypes = new Set(['Function', 'Class', 'Method', 'Interface']);
const clusterCenter = communityIndex !== undefined ? clusterCenters.get(communityIndex) : null;
if (parentPos) {
// Position near parent with small random offset
x = parentPos.x + (Math.random() - 0.5) * childJitter;
y = parentPos.y + (Math.random() - 0.5) * childJitter;
if (clusterCenter && symbolTypes.has(node.label)) {
// CLUSTER-BASED POSITIONING: Position near cluster center with tight jitter
x = clusterCenter.x + (Math.random() - 0.5) * clusterJitter;
y = clusterCenter.y + (Math.random() - 0.5) * clusterJitter;
} else {
// No parent found - position randomly but still spread out
x = (Math.random() - 0.5) * structuralSpread * 0.5;
y = (Math.random() - 0.5) * structuralSpread * 0.5;
// HIERARCHY-BASED POSITIONING: Position near parent
const parentId = childToParent.get(nodeId);
const parentPos = parentId ? nodePositions.get(parentId) : null;
if (parentPos) {
x = parentPos.x + (Math.random() - 0.5) * childJitter;
y = parentPos.y + (Math.random() - 0.5) * childJitter;
} else {
// No parent found - position randomly but still spread out
x = (Math.random() - 0.5) * structuralSpread * 0.5;
y = (Math.random() - 0.5) * structuralSpread * 0.5;
}
}
nodePositions.set(nodeId, { x, y });
@ -180,11 +222,20 @@ export const knowledgeGraphToGraphology = (
const baseSize = NODE_SIZES[node.label] || 8;
const scaledSize = getScaledNodeSize(baseSize, nodeCount);
// Check if this node has a community assignment (reuse communityIndex from above)
const hasCommunity = communityIndex !== undefined;
// Symbol nodes get colored by community if available
const usesCommunityColor = hasCommunity && symbolTypes.has(node.label);
const nodeColor = usesCommunityColor
? getCommunityColor(communityIndex!)
: NODE_COLORS[node.label] || '#9ca3af';
graph.addNode(nodeId, {
x,
y,
size: scaledSize,
color: NODE_COLORS[node.label] || '#9ca3af',
color: nodeColor,
label: node.properties.name,
nodeType: node.label,
filePath: node.properties.filePath,
@ -192,6 +243,8 @@ export const knowledgeGraphToGraphology = (
endLine: node.properties.endLine,
hidden: false,
mass: getNodeMass(node.label, nodeCount),
community: communityIndex,
communityColor: hasCommunity ? getCommunityColor(communityIndex!) : undefined,
});
};

13681
gitnexus/src/repomix-output.md Normal file

File diff suppressed because it is too large Load diff

View file

@ -17,25 +17,22 @@ const initFS = () => {
return fsName;
};
// Use public proxy in development, a custom proxy in production
const USE_OWN_PROXY = !import.meta.env.DEV;
// Hosted proxy URL - use this for localhost to avoid local proxy issues
const HOSTED_PROXY_URL = 'https://gitnexus.vercel.app/api/proxy';
/**
* Custom HTTP client that uses a query-param based proxy in production
* isomorphic-git's default corsProxy appends URL as path, which doesn't work
* well with Vercel's file-based routing.
* Custom HTTP client that uses a query-param based proxy
* - In development (localhost): uses the hosted Vercel proxy for reliability
* - In production: uses the local /api/proxy endpoint
*/
const createProxiedHttp = (): typeof http => {
if (!USE_OWN_PROXY) {
// In dev, use the public proxy via isomorphic-git's built-in corsProxy option
return http;
}
// In production, wrap the HTTP client to use the custom proxy
const isDev = typeof window !== 'undefined' && window.location.hostname === 'localhost';
return {
request: async (config) => {
// Rewrite the URL to go through the proxy
const proxyUrl = `/api/proxy?url=${encodeURIComponent(config.url)}`;
// Use hosted proxy for localhost, local proxy for production
const proxyBase = isDev ? HOSTED_PROXY_URL : '/api/proxy';
const proxyUrl = `${proxyBase}?url=${encodeURIComponent(config.url)}`;
// Call the original http.request with the proxied URL
return http.request({
@ -100,9 +97,6 @@ export const cloneRepository = async (
http: httpClient,
dir,
url: repoUrl,
// Only use corsProxy in dev mode (with public proxy)
...(import.meta.env.DEV ? { corsProxy: 'https://cors.isomorphic-git.org' } : {}),
singleBranch: true,
depth: 1,
// Auth callback for private repos (PAT stays client-side)
onAuth: token ? () => ({ username: token, password: 'x-oauth-basic' }) : undefined,

View file

@ -1,6 +1,7 @@
import { GraphNode, GraphRelationship, KnowledgeGraph } from '../core/graph/types';
import { CommunityDetectionResult } from '../core/ingestion/community-processor';
export type PipelinePhase = 'idle' | 'extracting' | 'structure' | 'parsing' | 'imports' | 'calls' | 'heritage' | 'complete' | 'error';
export type PipelinePhase = 'idle' | 'extracting' | 'structure' | 'parsing' | 'imports' | 'calls' | 'heritage' | 'communities' | 'enriching' | 'complete' | 'error';
export interface PipelineProgress {
phase: PipelinePhase;
@ -18,6 +19,7 @@ export interface PipelineProgress {
export interface PipelineResult {
graph: KnowledgeGraph;
fileContents: Map<string, string>;
communityResult?: CommunityDetectionResult;
}
// Serializable version for Web Worker communication

View file

@ -11,7 +11,11 @@ import {
import { isEmbedderReady, disposeEmbedder } from '../core/embeddings/embedder';
import type { EmbeddingProgress, SemanticSearchResult } from '../core/embeddings/types';
import type { ProviderConfig, AgentStreamChunk } from '../core/llm/types';
import { createGraphRAGAgent, streamAgentResponse, type AgentMessage } from '../core/llm/agent';
import { createGraphRAGAgent, streamAgentResponse, type AgentMessage, createChatModel } from '../core/llm/agent';
import { SystemMessage } from '@langchain/core/messages';
import { enrichClustersBatch, ClusterMemberInfo, ClusterEnrichment } from '../core/ingestion/cluster-enricher';
import { CommunityNode } from '../core/ingestion/community-processor';
import { PipelineResult } from '../types/pipeline';
import { buildCodebaseContext } from '../core/llm/context-builder';
import {
buildBM25Index,
@ -41,6 +45,11 @@ let storedFileContents: Map<string, string> = new Map();
// Agent state
let currentAgent: ReturnType<typeof createGraphRAGAgent> | null = null;
let currentProviderConfig: ProviderConfig | null = null;
let currentGraphResult: PipelineResult | null = null;
// Pending enrichment config (for background processing)
let pendingEnrichmentConfig: ProviderConfig | null = null;
let enrichmentCancelled = false;
/**
* Worker API exposed via Comlink
@ -57,10 +66,14 @@ const workerApi = {
*/
async runPipeline(
file: File,
onProgress: (progress: PipelineProgress) => void
onProgress: (progress: PipelineProgress) => void,
clusteringConfig?: ProviderConfig
): Promise<SerializablePipelineResult> {
// Debug logging
console.log('🔧 runPipeline called with clusteringConfig:', !!clusteringConfig);
// Run the actual pipeline
const result = await runIngestionPipeline(file, onProgress);
currentGraphResult = result;
// Store file contents for grep/read tools (full content, not truncated)
storedFileContents = result.fileContents;
@ -96,6 +109,12 @@ const workerApi = {
// KuzuDB is optional - silently continue without it
}
// Store clustering config for background enrichment (runs after graph loads)
if (clusteringConfig) {
pendingEnrichmentConfig = clusteringConfig;
console.log('📋 Clustering config saved for background enrichment');
}
// Convert to serializable format for transfer back to main thread
return serializePipelineResult(result);
},
@ -145,7 +164,8 @@ const workerApi = {
*/
async runPipelineFromFiles(
files: FileEntry[],
onProgress: (progress: PipelineProgress) => void
onProgress: (progress: PipelineProgress) => void,
clusteringConfig?: ProviderConfig
): Promise<SerializablePipelineResult> {
// Skip extraction phase, start from 15%
onProgress({
@ -157,6 +177,7 @@ const workerApi = {
// Run the pipeline
const result = await runPipelineFromFiles(files, onProgress);
currentGraphResult = result;
// Store file contents for grep/read tools (full content, not truncated)
storedFileContents = result.fileContents;
@ -192,6 +213,12 @@ const workerApi = {
// KuzuDB is optional - silently continue without it
}
// Store clustering config for background enrichment (runs after graph loads)
if (clusteringConfig) {
pendingEnrichmentConfig = clusteringConfig;
console.log('📋 Clustering config saved for background enrichment');
}
// Convert to serializable format for transfer back to main thread
return serializePipelineResult(result);
},
@ -235,6 +262,44 @@ const workerApi = {
);
},
/**
* Start background cluster enrichment (if pending)
* Called after graph loads, runs in background like embeddings
* @param onProgress - Progress callback
*/
async startBackgroundEnrichment(
onProgress?: (current: number, total: number) => void
): Promise<{ enriched: number; skipped: boolean }> {
if (!pendingEnrichmentConfig) {
console.log('⏭️ No pending enrichment config, skipping');
return { enriched: 0, skipped: true };
}
console.log('✨ Starting background LLM enrichment...');
try {
await workerApi.enrichCommunities(
pendingEnrichmentConfig,
onProgress ?? (() => {})
);
pendingEnrichmentConfig = null; // Clear after running
console.log('✅ Background enrichment completed');
return { enriched: 1, skipped: false };
} catch (err) {
console.error('❌ Background enrichment failed:', err);
pendingEnrichmentConfig = null;
return { enriched: 0, skipped: false };
}
},
/**
* Cancel the current enrichment operation
*/
async cancelEnrichment(): Promise<void> {
enrichmentCancelled = true;
pendingEnrichmentConfig = null;
console.log('⏸️ Enrichment cancelled by user');
},
/**
* Perform semantic search on the codebase
* @param query - Natural language search query
@ -522,6 +587,137 @@ const workerApi = {
currentAgent = null;
currentProviderConfig = null;
},
/**
* Enrich community clusters using LLM
*/
async enrichCommunities(
providerConfig: ProviderConfig,
onProgress: (current: number, total: number) => void
): Promise<{ enrichments: Record<string, ClusterEnrichment>, tokensUsed: number }> {
if (!currentGraphResult) {
throw new Error('No graph loaded. Please ingest a repository first.');
}
const { graph } = currentGraphResult;
// Filter for community nodes
const communityNodes = graph.nodes
.filter(n => n.label === 'Community')
.map(n => ({
id: n.id,
label: 'Community',
heuristicLabel: n.properties.heuristicLabel,
cohesion: n.properties.cohesion,
symbolCount: n.properties.symbolCount
} as CommunityNode));
if (communityNodes.length === 0) {
return { enrichments: {}, tokensUsed: 0 };
}
// Build member map: CommunityID -> Member Info
const memberMap = new Map<string, ClusterMemberInfo[]>();
// Initialize map
communityNodes.forEach(c => memberMap.set(c.id, []));
// Find all MEMBER_OF edges
graph.relationships.forEach(rel => {
if (rel.type === 'MEMBER_OF') {
const communityId = rel.targetId;
const memberId = rel.sourceId; // MEMBER_OF goes Member -> Community
if (memberMap.has(communityId)) {
// Find member node details
const memberNode = graph.nodes.find(n => n.id === memberId);
if (memberNode) {
memberMap.get(communityId)?.push({
name: memberNode.properties.name,
filePath: memberNode.properties.filePath,
type: memberNode.label
});
}
}
}
});
// Create LLM client adapter for LangChain model
const chatModel = createChatModel(providerConfig);
const llmClient = {
generate: async (prompt: string): Promise<string> => {
const response = await chatModel.invoke([
new SystemMessage('You are a helpful code analysis assistant.'),
{ role: 'user', content: prompt }
]);
return response.content as string;
}
};
// Run enrichment
const { enrichments, tokensUsed } = await enrichClustersBatch(
communityNodes,
memberMap,
llmClient,
5, // Batch size
onProgress
);
if (import.meta.env.DEV) {
console.log(`✨ Enriched ${enrichments.size} clusters using ~${Math.round(tokensUsed)} tokens`);
}
// Update graph nodes with enrichment data
graph.nodes.forEach(node => {
if (node.label === 'Community' && enrichments.has(node.id)) {
const enrichment = enrichments.get(node.id)!;
node.properties.name = enrichment.name; // Update display label
node.properties.keywords = enrichment.keywords;
node.properties.description = enrichment.description;
node.properties.enrichedBy = 'llm';
}
});
// Update KuzuDB with new data
try {
const kuzu = await getKuzuAdapter();
onProgress(enrichments.size, enrichments.size); // Done
// Update one by one via Cypher (simplest for now)
for (const [id, enrichment] of enrichments.entries()) {
// Escape strings for Cypher - replace backslash first, then quotes
const escapeCypher = (str: string) => str.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
const keywordsStr = JSON.stringify(enrichment.keywords);
const descStr = escapeCypher(enrichment.description);
const nameStr = escapeCypher(enrichment.name);
const escapedId = escapeCypher(id);
const query = `
MATCH (c:Community {id: "${escapedId}"})
SET c.label = "${nameStr}",
c.keywords = ${keywordsStr},
c.description = "${descStr}",
c.enrichedBy = "llm"
`;
await kuzu.executeQuery(query);
}
} catch (err) {
console.error('Failed to update KuzuDB with enrichment:', err);
}
// Convert Map to Record for serialization
const enrichmentsRecord: Record<string, ClusterEnrichment> = {};
for (const [id, val] of enrichments.entries()) {
enrichmentsRecord[id] = val;
}
return { enrichments: enrichmentsRecord, tokensUsed };
},
};
// Expose the worker API to the main thread