mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-09-08 22:22:52 +00:00
Merge pull request #31 from abhigyanpatwari/leidens_algorithm_and_processmap
Leidens algorithm and processmap
This commit is contained in:
commit
d02f861309
40 changed files with 16294 additions and 532 deletions
282
gitnexus-mcp/package-lock.json
generated
282
gitnexus-mcp/package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
453
gitnexus/TODO.md
453
gitnexus/TODO.md
|
|
@ -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
|
||||
|
|
|
|||
64
gitnexus/package-lock.json
generated
64
gitnexus/package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
BIN
gitnexus/public/wasm/c/tree-sitter-c.wasm
Normal file
BIN
gitnexus/public/wasm/c/tree-sitter-c.wasm
Normal file
Binary file not shown.
BIN
gitnexus/public/wasm/cpp/tree-sitter-cpp.wasm
Normal file
BIN
gitnexus/public/wasm/cpp/tree-sitter-cpp.wasm
Normal file
Binary file not shown.
BIN
gitnexus/public/wasm/csharp/tree-sitter-csharp.wasm
Normal file
BIN
gitnexus/public/wasm/csharp/tree-sitter-csharp.wasm
Normal file
Binary file not shown.
BIN
gitnexus/public/wasm/go/tree-sitter-go.wasm
Normal file
BIN
gitnexus/public/wasm/go/tree-sitter-go.wasm
Normal file
Binary file not shown.
BIN
gitnexus/public/wasm/java/tree-sitter-java.wasm
Normal file
BIN
gitnexus/public/wasm/java/tree-sitter-java.wasm
Normal file
Binary file not shown.
BIN
gitnexus/public/wasm/rust/tree-sitter-rust.wasm
Normal file
BIN
gitnexus/public/wasm/rust/tree-sitter-rust.wasm
Normal file
Binary file not shown.
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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}%` }}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
|
||||
|
|
|
|||
140
gitnexus/src/components/IntelligentClusteringModal.tsx
Normal file
140
gitnexus/src/components/IntelligentClusteringModal.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
]);
|
||||
|
||||
/**
|
||||
|
|
|
|||
245
gitnexus/src/core/ingestion/cluster-enricher.ts
Normal file
245
gitnexus/src/core/ingestion/cluster-enricher.ts
Normal 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 };
|
||||
};
|
||||
354
gitnexus/src/core/ingestion/community-processor.ts
Normal file
354
gitnexus/src/core/ingestion/community-processor.ts
Normal 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);
|
||||
};
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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}`);
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
||||
|
|
@ -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;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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)`;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 = [
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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}`);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
10
gitnexus/src/hooks/useSettings.ts
Normal file
10
gitnexus/src/hooks/useSettings.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import { useAppState } from './useAppState';
|
||||
|
||||
export const useSettings = () => {
|
||||
const { llmSettings, updateLLMSettings } = useAppState();
|
||||
|
||||
return {
|
||||
settings: llmSettings,
|
||||
updateSettings: updateLLMSettings
|
||||
};
|
||||
};
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
13681
gitnexus/src/repomix-output.md
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue