@vohongtho.infotech/code-intel 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -2
- package/dist/cli/main.js +2550 -933
- package/dist/cli/main.js.map +1 -1
- package/dist/index.d.ts +9 -3
- package/dist/index.js +1377 -216
- package/dist/index.js.map +1 -1
- package/dist/web/assets/{es-CnPQcqTr.js → es-yDTUrrnL.js} +1 -1
- package/dist/web/assets/{index-j-iO6isa.js → index-B4bH2ZP8.js} +4 -4
- package/dist/web/assets/index-DSIgTcZc.css +2 -0
- package/dist/web/index.html +2 -2
- package/package.json +12 -1
- package/dist/web/assets/index-rprt8Su_.css +0 -2
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createRequire } from 'module';
|
|
2
2
|
import { fileURLToPath } from 'url';
|
|
3
|
-
import
|
|
4
|
-
import
|
|
3
|
+
import path29 from 'path';
|
|
4
|
+
import fs23, { existsSync } from 'fs';
|
|
5
5
|
import { Parser, Language, Query } from 'web-tree-sitter';
|
|
6
6
|
import { NodeSDK } from '@opentelemetry/sdk-node';
|
|
7
7
|
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
|
|
@@ -126,11 +126,11 @@ var init_shared = __esm({
|
|
|
126
126
|
}
|
|
127
127
|
});
|
|
128
128
|
function findBundledWasmDir() {
|
|
129
|
-
const fileDir =
|
|
129
|
+
const fileDir = path29.dirname(fileURLToPath(import.meta.url));
|
|
130
130
|
const candidates = [
|
|
131
|
-
|
|
131
|
+
path29.join(fileDir, "wasm"),
|
|
132
132
|
// dist/index.js → dist/wasm/
|
|
133
|
-
|
|
133
|
+
path29.join(fileDir, "../wasm")
|
|
134
134
|
// dist/cli/main.js → dist/wasm/
|
|
135
135
|
];
|
|
136
136
|
for (const candidate of candidates) {
|
|
@@ -171,7 +171,7 @@ function wasmPath(lang) {
|
|
|
171
171
|
}
|
|
172
172
|
const bundled = BUNDLED_WASM_MAP[lang];
|
|
173
173
|
if (bundled) {
|
|
174
|
-
const bundledPath =
|
|
174
|
+
const bundledPath = path29.join(_bundledWasmDir, bundled);
|
|
175
175
|
if (existsSync(bundledPath)) return bundledPath;
|
|
176
176
|
}
|
|
177
177
|
return null;
|
|
@@ -184,14 +184,14 @@ async function initParser() {
|
|
|
184
184
|
}
|
|
185
185
|
async function getLanguage(lang) {
|
|
186
186
|
if (languageCache.has(lang)) return languageCache.get(lang);
|
|
187
|
-
const
|
|
188
|
-
if (!
|
|
187
|
+
const path30 = wasmPath(lang);
|
|
188
|
+
if (!path30) {
|
|
189
189
|
languageCache.set(lang, null);
|
|
190
190
|
return null;
|
|
191
191
|
}
|
|
192
192
|
try {
|
|
193
193
|
await initParser();
|
|
194
|
-
const language = await Language.load(
|
|
194
|
+
const language = await Language.load(path30);
|
|
195
195
|
languageCache.set(lang, language);
|
|
196
196
|
return language;
|
|
197
197
|
} catch {
|
|
@@ -1162,7 +1162,7 @@ var init_logger = __esm({
|
|
|
1162
1162
|
};
|
|
1163
1163
|
}
|
|
1164
1164
|
/** Global log directory: ~/.code-intel/logs */
|
|
1165
|
-
static LOG_DIR =
|
|
1165
|
+
static LOG_DIR = path29.join(os12.homedir(), ".code-intel", "logs");
|
|
1166
1166
|
static getLogger() {
|
|
1167
1167
|
if (!_Logger.instance) {
|
|
1168
1168
|
const isProduction = process.env.NODE_ENV === "production";
|
|
@@ -1171,12 +1171,12 @@ var init_logger = __esm({
|
|
|
1171
1171
|
transports.push(new winston.transports.Console());
|
|
1172
1172
|
if (!isProduction) {
|
|
1173
1173
|
try {
|
|
1174
|
-
if (!
|
|
1175
|
-
|
|
1174
|
+
if (!fs23.existsSync(_Logger.LOG_DIR)) {
|
|
1175
|
+
fs23.mkdirSync(_Logger.LOG_DIR, { recursive: true });
|
|
1176
1176
|
}
|
|
1177
1177
|
transports.push(
|
|
1178
1178
|
new DailyRotateFile({
|
|
1179
|
-
filename:
|
|
1179
|
+
filename: path29.join(_Logger.LOG_DIR, "%DATE%-code-intel.log"),
|
|
1180
1180
|
datePattern: "YYYY-MM-DD",
|
|
1181
1181
|
maxSize: "20m",
|
|
1182
1182
|
maxFiles: "14d"
|
|
@@ -1961,7 +1961,7 @@ var init_parse_phase = __esm({
|
|
|
1961
1961
|
const batch = filePaths.slice(i, i + CONCURRENCY);
|
|
1962
1962
|
await Promise.all(batch.map(async (filePath) => {
|
|
1963
1963
|
try {
|
|
1964
|
-
const source = await
|
|
1964
|
+
const source = await fs23.promises.readFile(filePath, "utf-8");
|
|
1965
1965
|
context2.fileCache.set(filePath, source);
|
|
1966
1966
|
} catch {
|
|
1967
1967
|
}
|
|
@@ -1974,14 +1974,14 @@ var init_parse_phase = __esm({
|
|
|
1974
1974
|
const lang = detectLanguage(filePath);
|
|
1975
1975
|
if (!lang) {
|
|
1976
1976
|
if (context2.verbose) {
|
|
1977
|
-
const relativePath2 =
|
|
1977
|
+
const relativePath2 = path29.relative(context2.workspaceRoot, filePath);
|
|
1978
1978
|
logger_default.info(` [parse] skipped (no parser): ${relativePath2}`);
|
|
1979
1979
|
}
|
|
1980
1980
|
continue;
|
|
1981
1981
|
}
|
|
1982
1982
|
const source = context2.fileCache.get(filePath);
|
|
1983
1983
|
if (!source) continue;
|
|
1984
|
-
const relativePath =
|
|
1984
|
+
const relativePath = path29.relative(context2.workspaceRoot, filePath);
|
|
1985
1985
|
const fileNodeId = generateNodeId("file", relativePath, relativePath);
|
|
1986
1986
|
const fileNode = context2.graph.getNode(fileNodeId);
|
|
1987
1987
|
if (fileNode) {
|
|
@@ -2221,11 +2221,11 @@ var init_resolve_phase = __esm({
|
|
|
2221
2221
|
let heritageEdges = 0;
|
|
2222
2222
|
const fileIndex = /* @__PURE__ */ new Map();
|
|
2223
2223
|
for (const fp of filePaths) {
|
|
2224
|
-
const rel =
|
|
2224
|
+
const rel = path29.relative(workspaceRoot, fp);
|
|
2225
2225
|
fileIndex.set(rel, fp);
|
|
2226
2226
|
const noExt = rel.replace(/\.\w+$/, "");
|
|
2227
2227
|
if (!fileIndex.has(noExt)) fileIndex.set(noExt, fp);
|
|
2228
|
-
const base =
|
|
2228
|
+
const base = path29.basename(rel, path29.extname(rel));
|
|
2229
2229
|
if (!fileIndex.has(base)) fileIndex.set(base, fp);
|
|
2230
2230
|
}
|
|
2231
2231
|
const symbolIndex = /* @__PURE__ */ new Map();
|
|
@@ -2256,7 +2256,7 @@ var init_resolve_phase = __esm({
|
|
|
2256
2256
|
for (const filePath of filePaths) {
|
|
2257
2257
|
const lang = detectLanguage(filePath);
|
|
2258
2258
|
if (!lang) continue;
|
|
2259
|
-
const relativePath =
|
|
2259
|
+
const relativePath = path29.relative(workspaceRoot, filePath);
|
|
2260
2260
|
const fileNodeId = generateNodeId("file", relativePath, relativePath);
|
|
2261
2261
|
const source = fileCache.get(filePath);
|
|
2262
2262
|
if (!source) continue;
|
|
@@ -2269,13 +2269,13 @@ var init_resolve_phase = __esm({
|
|
|
2269
2269
|
let resolvedRelPath = null;
|
|
2270
2270
|
if (cleaned.startsWith(".")) {
|
|
2271
2271
|
const cleanedNoJs = cleaned.replace(/\.(js|jsx)$/, "");
|
|
2272
|
-
const fromDir =
|
|
2272
|
+
const fromDir = path29.dirname(relativePath);
|
|
2273
2273
|
for (const ext of ["", ".ts", ".tsx", ".js", ".jsx", ".py", ".java", ".go", "/index.ts", "/index.js"]) {
|
|
2274
|
-
const candidate =
|
|
2275
|
-
const normalized =
|
|
2274
|
+
const candidate = path29.join(fromDir, cleanedNoJs + ext);
|
|
2275
|
+
const normalized = path29.normalize(candidate);
|
|
2276
2276
|
if (fileIndex.has(normalized)) {
|
|
2277
2277
|
const absPath = fileIndex.get(normalized);
|
|
2278
|
-
resolvedRelPath =
|
|
2278
|
+
resolvedRelPath = path29.relative(workspaceRoot, absPath);
|
|
2279
2279
|
break;
|
|
2280
2280
|
}
|
|
2281
2281
|
}
|
|
@@ -2589,27 +2589,27 @@ __export(group_registry_exports, {
|
|
|
2589
2589
|
saveSyncResult: () => saveSyncResult
|
|
2590
2590
|
});
|
|
2591
2591
|
function groupFile(name) {
|
|
2592
|
-
return
|
|
2592
|
+
return path29.join(GROUPS_DIR, `${name}.json`);
|
|
2593
2593
|
}
|
|
2594
2594
|
function loadGroup(name) {
|
|
2595
2595
|
try {
|
|
2596
|
-
return JSON.parse(
|
|
2596
|
+
return JSON.parse(fs23.readFileSync(groupFile(name), "utf-8"));
|
|
2597
2597
|
} catch {
|
|
2598
2598
|
return null;
|
|
2599
2599
|
}
|
|
2600
2600
|
}
|
|
2601
2601
|
function saveGroup(group) {
|
|
2602
|
-
|
|
2603
|
-
|
|
2602
|
+
fs23.mkdirSync(GROUPS_DIR, { recursive: true });
|
|
2603
|
+
fs23.writeFileSync(groupFile(group.name), JSON.stringify(group, null, 2) + "\n");
|
|
2604
2604
|
}
|
|
2605
2605
|
function listGroups() {
|
|
2606
2606
|
const groups = [];
|
|
2607
2607
|
try {
|
|
2608
|
-
for (const file of
|
|
2608
|
+
for (const file of fs23.readdirSync(GROUPS_DIR)) {
|
|
2609
2609
|
if (!file.endsWith(".json") || file.endsWith(".sync.json")) continue;
|
|
2610
2610
|
try {
|
|
2611
2611
|
const g = JSON.parse(
|
|
2612
|
-
|
|
2612
|
+
fs23.readFileSync(path29.join(GROUPS_DIR, file), "utf-8")
|
|
2613
2613
|
);
|
|
2614
2614
|
groups.push(g);
|
|
2615
2615
|
} catch {
|
|
@@ -2621,16 +2621,16 @@ function listGroups() {
|
|
|
2621
2621
|
}
|
|
2622
2622
|
function deleteGroup(name) {
|
|
2623
2623
|
try {
|
|
2624
|
-
|
|
2624
|
+
fs23.unlinkSync(groupFile(name));
|
|
2625
2625
|
} catch {
|
|
2626
2626
|
}
|
|
2627
2627
|
try {
|
|
2628
|
-
|
|
2628
|
+
fs23.unlinkSync(path29.join(GROUPS_DIR, `${name}.sync.json`));
|
|
2629
2629
|
} catch {
|
|
2630
2630
|
}
|
|
2631
2631
|
}
|
|
2632
2632
|
function groupExists(name) {
|
|
2633
|
-
return
|
|
2633
|
+
return fs23.existsSync(groupFile(name));
|
|
2634
2634
|
}
|
|
2635
2635
|
function addMember(groupName, member) {
|
|
2636
2636
|
const group = loadGroup(groupName);
|
|
@@ -2656,16 +2656,16 @@ function removeMember(groupName, groupPath) {
|
|
|
2656
2656
|
return group;
|
|
2657
2657
|
}
|
|
2658
2658
|
function saveSyncResult(result) {
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2659
|
+
fs23.mkdirSync(GROUPS_DIR, { recursive: true });
|
|
2660
|
+
fs23.writeFileSync(
|
|
2661
|
+
path29.join(GROUPS_DIR, `${result.groupName}.sync.json`),
|
|
2662
2662
|
JSON.stringify(result, null, 2) + "\n"
|
|
2663
2663
|
);
|
|
2664
2664
|
}
|
|
2665
2665
|
function loadSyncResult(groupName) {
|
|
2666
2666
|
try {
|
|
2667
2667
|
return JSON.parse(
|
|
2668
|
-
|
|
2668
|
+
fs23.readFileSync(path29.join(GROUPS_DIR, `${groupName}.sync.json`), "utf-8")
|
|
2669
2669
|
);
|
|
2670
2670
|
} catch {
|
|
2671
2671
|
return null;
|
|
@@ -2674,7 +2674,7 @@ function loadSyncResult(groupName) {
|
|
|
2674
2674
|
var GROUPS_DIR;
|
|
2675
2675
|
var init_group_registry = __esm({
|
|
2676
2676
|
"src/multi-repo/group-registry.ts"() {
|
|
2677
|
-
GROUPS_DIR =
|
|
2677
|
+
GROUPS_DIR = path29.join(os12.homedir(), ".code-intel", "groups");
|
|
2678
2678
|
}
|
|
2679
2679
|
});
|
|
2680
2680
|
|
|
@@ -2927,9 +2927,9 @@ var init_orphan_files = __esm({
|
|
|
2927
2927
|
// src/health/health-score.ts
|
|
2928
2928
|
var health_score_exports = {};
|
|
2929
2929
|
__export(health_score_exports, {
|
|
2930
|
-
computeHealthReport: () =>
|
|
2930
|
+
computeHealthReport: () => computeHealthReport2
|
|
2931
2931
|
});
|
|
2932
|
-
function
|
|
2932
|
+
function computeHealthReport2(graph, godNodeConfig) {
|
|
2933
2933
|
const deadCode = detectDeadCode(graph);
|
|
2934
2934
|
const cycles = detectCircularDeps(graph);
|
|
2935
2935
|
const godNodes = detectGodNodes(graph, godNodeConfig);
|
|
@@ -3735,10 +3735,10 @@ var init_codes = __esm({
|
|
|
3735
3735
|
}
|
|
3736
3736
|
});
|
|
3737
3737
|
function secureMkdir(dir) {
|
|
3738
|
-
|
|
3738
|
+
fs23.mkdirSync(dir, { recursive: true, mode: SECURE_DIR_MODE });
|
|
3739
3739
|
if (process.platform !== "win32") {
|
|
3740
3740
|
try {
|
|
3741
|
-
|
|
3741
|
+
fs23.chmodSync(dir, SECURE_DIR_MODE);
|
|
3742
3742
|
} catch {
|
|
3743
3743
|
}
|
|
3744
3744
|
}
|
|
@@ -3746,17 +3746,17 @@ function secureMkdir(dir) {
|
|
|
3746
3746
|
function secureChmodFile(file) {
|
|
3747
3747
|
if (process.platform === "win32") return;
|
|
3748
3748
|
try {
|
|
3749
|
-
|
|
3749
|
+
fs23.chmodSync(file, SECURE_FILE_MODE);
|
|
3750
3750
|
} catch {
|
|
3751
3751
|
}
|
|
3752
3752
|
}
|
|
3753
3753
|
function tightenDbFiles(dir) {
|
|
3754
3754
|
if (process.platform === "win32") return;
|
|
3755
|
-
if (!
|
|
3756
|
-
for (const name of
|
|
3755
|
+
if (!fs23.existsSync(dir)) return;
|
|
3756
|
+
for (const name of fs23.readdirSync(dir)) {
|
|
3757
3757
|
if (name.endsWith(".db") || name.endsWith(".db-wal") || name.endsWith(".db-shm")) {
|
|
3758
3758
|
try {
|
|
3759
|
-
|
|
3759
|
+
fs23.chmodSync(path29.join(dir, name), SECURE_FILE_MODE);
|
|
3760
3760
|
} catch {
|
|
3761
3761
|
}
|
|
3762
3762
|
}
|
|
@@ -3770,7 +3770,7 @@ var init_fs_secure = __esm({
|
|
|
3770
3770
|
}
|
|
3771
3771
|
});
|
|
3772
3772
|
function getUsersDBPath() {
|
|
3773
|
-
return process.env["CODE_INTEL_USERS_DB_PATH"] ??
|
|
3773
|
+
return process.env["CODE_INTEL_USERS_DB_PATH"] ?? path29.join(os12.homedir(), ".code-intel", "users.db");
|
|
3774
3774
|
}
|
|
3775
3775
|
function getOrCreateUsersDB() {
|
|
3776
3776
|
if (!_usersDB) {
|
|
@@ -3786,7 +3786,7 @@ var init_users_db = __esm({
|
|
|
3786
3786
|
UsersDB = class {
|
|
3787
3787
|
db;
|
|
3788
3788
|
constructor(dbPath) {
|
|
3789
|
-
const dir =
|
|
3789
|
+
const dir = path29.dirname(dbPath);
|
|
3790
3790
|
secureMkdir(dir);
|
|
3791
3791
|
this.db = new Database3(dbPath);
|
|
3792
3792
|
this.db.pragma("journal_mode = WAL");
|
|
@@ -4063,7 +4063,7 @@ function getScryptN() {
|
|
|
4063
4063
|
return Number.isInteger(v) && v >= 1024 ? v : 1 << 14;
|
|
4064
4064
|
}
|
|
4065
4065
|
function getSecretsPath() {
|
|
4066
|
-
return process.env["CODE_INTEL_SECRETS_PATH"] ??
|
|
4066
|
+
return process.env["CODE_INTEL_SECRETS_PATH"] ?? path29.join(os12.homedir(), ".code-intel", ".secrets");
|
|
4067
4067
|
}
|
|
4068
4068
|
function getMasterPassword() {
|
|
4069
4069
|
const fromEnv = process.env["CODE_INTEL_SECRET_KEY"];
|
|
@@ -4094,8 +4094,8 @@ function decryptSecrets(encrypted) {
|
|
|
4094
4094
|
return JSON.parse(plaintext.toString("utf8"));
|
|
4095
4095
|
}
|
|
4096
4096
|
function loadSecrets(secretsPath = getSecretsPath()) {
|
|
4097
|
-
if (!
|
|
4098
|
-
const blob =
|
|
4097
|
+
if (!fs23.existsSync(secretsPath)) return {};
|
|
4098
|
+
const blob = fs23.readFileSync(secretsPath);
|
|
4099
4099
|
return decryptSecrets(blob);
|
|
4100
4100
|
}
|
|
4101
4101
|
function getSecret(key, secretsPath = getSecretsPath()) {
|
|
@@ -4214,6 +4214,9 @@ function requireAuth(req, res, next) {
|
|
|
4214
4214
|
}
|
|
4215
4215
|
next();
|
|
4216
4216
|
}
|
|
4217
|
+
function meetsRole(userRole, required) {
|
|
4218
|
+
return (ROLE_RANK[userRole] ?? 0) >= (ROLE_RANK[required] ?? 0);
|
|
4219
|
+
}
|
|
4217
4220
|
function requireRole(...roles) {
|
|
4218
4221
|
return (req, res, next) => {
|
|
4219
4222
|
if (!req.user) {
|
|
@@ -4228,7 +4231,8 @@ function requireRole(...roles) {
|
|
|
4228
4231
|
});
|
|
4229
4232
|
return;
|
|
4230
4233
|
}
|
|
4231
|
-
|
|
4234
|
+
const allowed = roles.some((r) => meetsRole(req.user.role, r));
|
|
4235
|
+
if (!allowed) {
|
|
4232
4236
|
res.status(403).json({
|
|
4233
4237
|
error: {
|
|
4234
4238
|
code: ErrorCodes.FORBIDDEN,
|
|
@@ -4337,7 +4341,7 @@ function clearSessionCookie() {
|
|
|
4337
4341
|
async function verifyPassword(plain, hash) {
|
|
4338
4342
|
return bcrypt.compare(plain, hash);
|
|
4339
4343
|
}
|
|
4340
|
-
var sessionStore, SESSION_COOKIE_NAME;
|
|
4344
|
+
var sessionStore, SESSION_COOKIE_NAME, ROLE_RANK;
|
|
4341
4345
|
var init_middleware = __esm({
|
|
4342
4346
|
"src/auth/middleware.ts"() {
|
|
4343
4347
|
init_users_db();
|
|
@@ -4345,6 +4349,12 @@ var init_middleware = __esm({
|
|
|
4345
4349
|
init_secret_store();
|
|
4346
4350
|
sessionStore = /* @__PURE__ */ new Map();
|
|
4347
4351
|
SESSION_COOKIE_NAME = "code_intel_session";
|
|
4352
|
+
ROLE_RANK = {
|
|
4353
|
+
viewer: 1,
|
|
4354
|
+
"repo-owner": 2,
|
|
4355
|
+
analyst: 3,
|
|
4356
|
+
admin: 4
|
|
4357
|
+
};
|
|
4348
4358
|
}
|
|
4349
4359
|
});
|
|
4350
4360
|
function verifyWebSocketHandshake(req) {
|
|
@@ -4652,7 +4662,7 @@ init_shared();
|
|
|
4652
4662
|
init_shared();
|
|
4653
4663
|
init_typescript();
|
|
4654
4664
|
function resolveRelative(rawPath, fromFile, workspace) {
|
|
4655
|
-
const fromDir =
|
|
4665
|
+
const fromDir = path29.dirname(fromFile);
|
|
4656
4666
|
const cleaned = rawPath.replace(/['"]/g, "");
|
|
4657
4667
|
const extensions = [".ts", ".tsx", ".js", ".jsx", "/index.ts", "/index.js"];
|
|
4658
4668
|
const resolved = workspace.resolve(fromDir, cleaned);
|
|
@@ -4704,7 +4714,7 @@ var pythonModule = {
|
|
|
4704
4714
|
resolveImport(rawPath, fromFile, workspace) {
|
|
4705
4715
|
const cleaned = rawPath.replace(/['"]/g, "");
|
|
4706
4716
|
const parts = cleaned.split(".");
|
|
4707
|
-
const fromDir =
|
|
4717
|
+
const fromDir = path29.dirname(fromFile);
|
|
4708
4718
|
const relPath = parts.join("/");
|
|
4709
4719
|
for (const suffix of ["/__init__.py", ".py"]) {
|
|
4710
4720
|
const r = workspace.resolve(fromDir, relPath + suffix);
|
|
@@ -4783,7 +4793,7 @@ var cModule = {
|
|
|
4783
4793
|
inheritanceStrategy: "none",
|
|
4784
4794
|
resolveImport(rawPath, fromFile, workspace) {
|
|
4785
4795
|
const cleaned = rawPath.replace(/[<>"']/g, "");
|
|
4786
|
-
const fromDir =
|
|
4796
|
+
const fromDir = path29.dirname(fromFile);
|
|
4787
4797
|
return workspace.resolve(fromDir, cleaned);
|
|
4788
4798
|
},
|
|
4789
4799
|
isExported(_node) {
|
|
@@ -4806,7 +4816,7 @@ var cppModule = {
|
|
|
4806
4816
|
inheritanceStrategy: "depth-first",
|
|
4807
4817
|
resolveImport(rawPath, fromFile, workspace) {
|
|
4808
4818
|
const cleaned = rawPath.replace(/[<>"']/g, "");
|
|
4809
|
-
const fromDir =
|
|
4819
|
+
const fromDir = path29.dirname(fromFile);
|
|
4810
4820
|
return workspace.resolve(fromDir, cleaned);
|
|
4811
4821
|
},
|
|
4812
4822
|
isExported(_node) {
|
|
@@ -4968,7 +4978,7 @@ var dartModule = {
|
|
|
4968
4978
|
const pkg = cleaned.replace("package:", "");
|
|
4969
4979
|
return workspace.findByPackage(pkg);
|
|
4970
4980
|
}
|
|
4971
|
-
const fromDir =
|
|
4981
|
+
const fromDir = path29.dirname(fromFile);
|
|
4972
4982
|
return workspace.resolve(fromDir, cleaned);
|
|
4973
4983
|
},
|
|
4974
4984
|
isExported(node) {
|
|
@@ -5323,25 +5333,25 @@ function validateDAG(phases) {
|
|
|
5323
5333
|
const visiting = /* @__PURE__ */ new Set();
|
|
5324
5334
|
const visited = /* @__PURE__ */ new Set();
|
|
5325
5335
|
const phaseMap = new Map(phases.map((p) => [p.name, p]));
|
|
5326
|
-
function dfs(name,
|
|
5336
|
+
function dfs(name, path30) {
|
|
5327
5337
|
if (visiting.has(name)) {
|
|
5328
|
-
const cycleStart =
|
|
5329
|
-
const cycle =
|
|
5338
|
+
const cycleStart = path30.indexOf(name);
|
|
5339
|
+
const cycle = path30.slice(cycleStart).concat(name);
|
|
5330
5340
|
errors.push({ type: "cycle", message: `Cycle detected: ${cycle.join(" \u2192 ")}` });
|
|
5331
5341
|
return true;
|
|
5332
5342
|
}
|
|
5333
5343
|
if (visited.has(name)) return false;
|
|
5334
5344
|
visiting.add(name);
|
|
5335
|
-
|
|
5345
|
+
path30.push(name);
|
|
5336
5346
|
const phase = phaseMap.get(name);
|
|
5337
5347
|
if (phase) {
|
|
5338
5348
|
for (const dep of phase.dependencies) {
|
|
5339
|
-
if (dfs(dep,
|
|
5349
|
+
if (dfs(dep, path30)) return true;
|
|
5340
5350
|
}
|
|
5341
5351
|
}
|
|
5342
5352
|
visiting.delete(name);
|
|
5343
5353
|
visited.add(name);
|
|
5344
|
-
|
|
5354
|
+
path30.pop();
|
|
5345
5355
|
return false;
|
|
5346
5356
|
}
|
|
5347
5357
|
for (const phase of phases) {
|
|
@@ -5554,7 +5564,7 @@ var IGNORED_DIRS = /* @__PURE__ */ new Set([
|
|
|
5554
5564
|
]);
|
|
5555
5565
|
function loadIgnorePatterns(workspaceRoot) {
|
|
5556
5566
|
try {
|
|
5557
|
-
const raw =
|
|
5567
|
+
const raw = fs23.readFileSync(path29.join(workspaceRoot, ".codeintelignore"), "utf-8");
|
|
5558
5568
|
const extras = /* @__PURE__ */ new Set();
|
|
5559
5569
|
for (const line of raw.split("\n")) {
|
|
5560
5570
|
const trimmed = line.trim();
|
|
@@ -5578,7 +5588,7 @@ var scanPhase = {
|
|
|
5578
5588
|
function walk(dir) {
|
|
5579
5589
|
let entries;
|
|
5580
5590
|
try {
|
|
5581
|
-
entries =
|
|
5591
|
+
entries = fs23.readdirSync(dir, { withFileTypes: true });
|
|
5582
5592
|
} catch {
|
|
5583
5593
|
return;
|
|
5584
5594
|
}
|
|
@@ -5587,15 +5597,15 @@ var scanPhase = {
|
|
|
5587
5597
|
if (entry.name.startsWith(".")) continue;
|
|
5588
5598
|
if (IGNORED_DIRS.has(entry.name)) continue;
|
|
5589
5599
|
if (extraIgnore.has(entry.name)) continue;
|
|
5590
|
-
walk(
|
|
5600
|
+
walk(path29.join(dir, entry.name));
|
|
5591
5601
|
} else if (entry.isFile()) {
|
|
5592
5602
|
const name = entry.name;
|
|
5593
5603
|
if (IGNORED_FILE_SUFFIXES.some((s) => name.endsWith(s))) continue;
|
|
5594
|
-
const ext =
|
|
5604
|
+
const ext = path29.extname(name);
|
|
5595
5605
|
if (!extensions.has(ext)) continue;
|
|
5596
|
-
const fullPath =
|
|
5606
|
+
const fullPath = path29.join(dir, name);
|
|
5597
5607
|
try {
|
|
5598
|
-
const stat =
|
|
5608
|
+
const stat = fs23.statSync(fullPath);
|
|
5599
5609
|
if (stat.size > MAX_FILE_SIZE_BYTES) continue;
|
|
5600
5610
|
} catch {
|
|
5601
5611
|
continue;
|
|
@@ -5622,20 +5632,20 @@ var structurePhase = {
|
|
|
5622
5632
|
const dirs = /* @__PURE__ */ new Set();
|
|
5623
5633
|
let structDone = 0;
|
|
5624
5634
|
for (const filePath of context2.filePaths) {
|
|
5625
|
-
const relativePath =
|
|
5635
|
+
const relativePath = path29.relative(context2.workspaceRoot, filePath);
|
|
5626
5636
|
const lang = detectLanguage(filePath);
|
|
5627
5637
|
context2.graph.addNode({
|
|
5628
5638
|
id: generateNodeId("file", relativePath, relativePath),
|
|
5629
5639
|
kind: "file",
|
|
5630
|
-
name:
|
|
5640
|
+
name: path29.basename(filePath),
|
|
5631
5641
|
filePath: relativePath,
|
|
5632
5642
|
metadata: lang ? { language: lang } : void 0
|
|
5633
5643
|
});
|
|
5634
|
-
let dir =
|
|
5644
|
+
let dir = path29.dirname(relativePath);
|
|
5635
5645
|
while (dir && dir !== "." && dir !== "") {
|
|
5636
5646
|
if (dirs.has(dir)) break;
|
|
5637
5647
|
dirs.add(dir);
|
|
5638
|
-
dir =
|
|
5648
|
+
dir = path29.dirname(dir);
|
|
5639
5649
|
}
|
|
5640
5650
|
structDone++;
|
|
5641
5651
|
context2.onPhaseProgress?.("structure", structDone, context2.filePaths.length);
|
|
@@ -5644,7 +5654,7 @@ var structurePhase = {
|
|
|
5644
5654
|
context2.graph.addNode({
|
|
5645
5655
|
id: generateNodeId("directory", dir, dir),
|
|
5646
5656
|
kind: "directory",
|
|
5647
|
-
name:
|
|
5657
|
+
name: path29.basename(dir),
|
|
5648
5658
|
filePath: dir
|
|
5649
5659
|
});
|
|
5650
5660
|
}
|
|
@@ -5755,22 +5765,22 @@ var flowPhase = {
|
|
|
5755
5765
|
const queue = [{ nodeId: ep.id, path: [ep.id] }];
|
|
5756
5766
|
const visited = /* @__PURE__ */ new Set();
|
|
5757
5767
|
while (queue.length > 0 && flowCount < maxFlows) {
|
|
5758
|
-
const { nodeId, path:
|
|
5759
|
-
if (
|
|
5768
|
+
const { nodeId, path: path30 } = queue.shift();
|
|
5769
|
+
if (path30.length > maxDepth) continue;
|
|
5760
5770
|
const callEdges = [...graph.findEdgesFrom(nodeId)].filter((e) => e.kind === "calls").slice(0, maxBranching);
|
|
5761
|
-
if (callEdges.length === 0 &&
|
|
5771
|
+
if (callEdges.length === 0 && path30.length >= 3) {
|
|
5762
5772
|
const flowId = generateNodeId("flow", ep.filePath, `flow-${flowCount}`);
|
|
5763
5773
|
graph.addNode({
|
|
5764
5774
|
id: flowId,
|
|
5765
5775
|
kind: "flow",
|
|
5766
5776
|
name: `${ep.name} flow ${flowCount}`,
|
|
5767
5777
|
filePath: ep.filePath,
|
|
5768
|
-
metadata: { steps:
|
|
5778
|
+
metadata: { steps: path30, entryPoint: ep.name }
|
|
5769
5779
|
});
|
|
5770
|
-
for (let i = 0; i <
|
|
5780
|
+
for (let i = 0; i < path30.length; i++) {
|
|
5771
5781
|
graph.addEdge({
|
|
5772
|
-
id: generateEdgeId(
|
|
5773
|
-
source:
|
|
5782
|
+
id: generateEdgeId(path30[i], flowId, `step_of_${i}`),
|
|
5783
|
+
source: path30[i],
|
|
5774
5784
|
target: flowId,
|
|
5775
5785
|
kind: "step_of",
|
|
5776
5786
|
weight: 1,
|
|
@@ -5783,7 +5793,7 @@ var flowPhase = {
|
|
|
5783
5793
|
for (const edge of callEdges) {
|
|
5784
5794
|
if (visited.has(edge.target)) continue;
|
|
5785
5795
|
visited.add(edge.target);
|
|
5786
|
-
queue.push({ nodeId: edge.target, path: [...
|
|
5796
|
+
queue.push({ nodeId: edge.target, path: [...path30, edge.target] });
|
|
5787
5797
|
}
|
|
5788
5798
|
}
|
|
5789
5799
|
}
|
|
@@ -5801,7 +5811,7 @@ var LLMGovernanceLogger = class {
|
|
|
5801
5811
|
}
|
|
5802
5812
|
/** Path to the JSONL log file. */
|
|
5803
5813
|
getLogPath() {
|
|
5804
|
-
return process.env["CODE_INTEL_GOVERNANCE_LOG_PATH"] ??
|
|
5814
|
+
return process.env["CODE_INTEL_GOVERNANCE_LOG_PATH"] ?? path29.join(os12.homedir(), ".code-intel", "llm-governance.jsonl");
|
|
5805
5815
|
}
|
|
5806
5816
|
/**
|
|
5807
5817
|
* Append an entry to the governance log.
|
|
@@ -5817,8 +5827,8 @@ var LLMGovernanceLogger = class {
|
|
|
5817
5827
|
...entry
|
|
5818
5828
|
};
|
|
5819
5829
|
const logPath = this.getLogPath();
|
|
5820
|
-
|
|
5821
|
-
|
|
5830
|
+
fs23.mkdirSync(path29.dirname(logPath), { recursive: true });
|
|
5831
|
+
fs23.appendFileSync(logPath, JSON.stringify(full) + "\n", "utf-8");
|
|
5822
5832
|
} catch {
|
|
5823
5833
|
}
|
|
5824
5834
|
}
|
|
@@ -5828,7 +5838,7 @@ var LLMGovernanceLogger = class {
|
|
|
5828
5838
|
*/
|
|
5829
5839
|
readLog(limit = 100) {
|
|
5830
5840
|
try {
|
|
5831
|
-
const raw =
|
|
5841
|
+
const raw = fs23.readFileSync(this.getLogPath(), "utf-8");
|
|
5832
5842
|
const lines = raw.split("\n").filter((l) => l.trim().length > 0).slice(-limit);
|
|
5833
5843
|
return lines.map((l) => JSON.parse(l));
|
|
5834
5844
|
} catch {
|
|
@@ -5972,17 +5982,17 @@ function traceFlow(entryId, graph, maxDepth = 10, maxBranching = 4) {
|
|
|
5972
5982
|
const queue = [{ nodeId: entryId, path: [entryId] }];
|
|
5973
5983
|
const visited = /* @__PURE__ */ new Set();
|
|
5974
5984
|
while (queue.length > 0 && flows.length < maxFlows) {
|
|
5975
|
-
const { nodeId, path:
|
|
5976
|
-
if (
|
|
5985
|
+
const { nodeId, path: path30 } = queue.shift();
|
|
5986
|
+
if (path30.length > maxDepth) continue;
|
|
5977
5987
|
const callEdges = [...graph.findEdgesFrom(nodeId)].filter((e) => e.kind === "calls").slice(0, maxBranching);
|
|
5978
|
-
if (callEdges.length === 0 &&
|
|
5979
|
-
flows.push({ entryPointId: entryId, steps: [...
|
|
5988
|
+
if (callEdges.length === 0 && path30.length >= 3) {
|
|
5989
|
+
flows.push({ entryPointId: entryId, steps: [...path30] });
|
|
5980
5990
|
continue;
|
|
5981
5991
|
}
|
|
5982
5992
|
for (const edge of callEdges) {
|
|
5983
5993
|
if (visited.has(edge.target)) continue;
|
|
5984
5994
|
visited.add(edge.target);
|
|
5985
|
-
queue.push({ nodeId: edge.target, path: [...
|
|
5995
|
+
queue.push({ nodeId: edge.target, path: [...path30, edge.target] });
|
|
5986
5996
|
}
|
|
5987
5997
|
}
|
|
5988
5998
|
}
|
|
@@ -6216,7 +6226,7 @@ init_embedder();
|
|
|
6216
6226
|
async function hybridSearch(graph, query, limit, options = {}) {
|
|
6217
6227
|
const { vectorDbPath, bm25Limit = 50, vectorLimit = 50 } = options;
|
|
6218
6228
|
const bm25Promise = Promise.resolve(textSearch(graph, query, bm25Limit));
|
|
6219
|
-
const hasVectorDb = Boolean(vectorDbPath &&
|
|
6229
|
+
const hasVectorDb = Boolean(vectorDbPath && fs23.existsSync(vectorDbPath));
|
|
6220
6230
|
if (!hasVectorDb) {
|
|
6221
6231
|
const bm25Results2 = await bm25Promise;
|
|
6222
6232
|
return {
|
|
@@ -6273,7 +6283,7 @@ var DbManager = class {
|
|
|
6273
6283
|
this.dbPath = dbPath;
|
|
6274
6284
|
}
|
|
6275
6285
|
async init() {
|
|
6276
|
-
|
|
6286
|
+
fs23.mkdirSync(path29.dirname(this.dbPath), { recursive: true });
|
|
6277
6287
|
this.db = new Database(this.dbPath);
|
|
6278
6288
|
await this.db.init();
|
|
6279
6289
|
this.conn = new Connection(this.db);
|
|
@@ -6330,7 +6340,8 @@ var NODE_TABLE_MAP = {
|
|
|
6330
6340
|
constant: "const_nodes",
|
|
6331
6341
|
route: "route_nodes",
|
|
6332
6342
|
cluster: "cluster_nodes",
|
|
6333
|
-
flow: "flow_nodes"
|
|
6343
|
+
flow: "flow_nodes",
|
|
6344
|
+
vulnerability: "vuln_nodes"
|
|
6334
6345
|
};
|
|
6335
6346
|
var ALL_NODE_TABLES = [...new Set(Object.values(NODE_TABLE_MAP))];
|
|
6336
6347
|
function getCreateNodeTableDDL(tableName) {
|
|
@@ -6362,7 +6373,7 @@ function getCreateEdgeTableDDL() {
|
|
|
6362
6373
|
)`];
|
|
6363
6374
|
}
|
|
6364
6375
|
function writeNodeCSVs(graph, outputDir) {
|
|
6365
|
-
|
|
6376
|
+
fs23.mkdirSync(outputDir, { recursive: true });
|
|
6366
6377
|
const header = "id,name,file_path,start_line,end_line,exported,content,metadata\n";
|
|
6367
6378
|
const tableBuffers = /* @__PURE__ */ new Map();
|
|
6368
6379
|
const tableFilePaths = /* @__PURE__ */ new Map();
|
|
@@ -6370,7 +6381,7 @@ function writeNodeCSVs(graph, outputDir) {
|
|
|
6370
6381
|
const table = NODE_TABLE_MAP[node.kind];
|
|
6371
6382
|
if (!tableBuffers.has(table)) {
|
|
6372
6383
|
tableBuffers.set(table, [header]);
|
|
6373
|
-
tableFilePaths.set(table,
|
|
6384
|
+
tableFilePaths.set(table, path29.join(outputDir, `${table}.csv`));
|
|
6374
6385
|
}
|
|
6375
6386
|
tableBuffers.get(table).push(
|
|
6376
6387
|
csvRow([
|
|
@@ -6390,12 +6401,12 @@ function writeNodeCSVs(graph, outputDir) {
|
|
|
6390
6401
|
);
|
|
6391
6402
|
}
|
|
6392
6403
|
for (const [table, lines] of tableBuffers) {
|
|
6393
|
-
|
|
6404
|
+
fs23.writeFileSync(tableFilePaths.get(table), lines.join(""), "utf-8");
|
|
6394
6405
|
}
|
|
6395
6406
|
return tableFilePaths;
|
|
6396
6407
|
}
|
|
6397
6408
|
function writeEdgeCSV(graph, outputDir) {
|
|
6398
|
-
|
|
6409
|
+
fs23.mkdirSync(outputDir, { recursive: true });
|
|
6399
6410
|
const header = "from_id,to_id,kind,weight,label\n";
|
|
6400
6411
|
const groups = /* @__PURE__ */ new Map();
|
|
6401
6412
|
for (const edge of graph.allEdges()) {
|
|
@@ -6406,7 +6417,7 @@ function writeEdgeCSV(graph, outputDir) {
|
|
|
6406
6417
|
const toTable = NODE_TABLE_MAP[targetNode.kind];
|
|
6407
6418
|
const key = `${fromTable}->${toTable}`;
|
|
6408
6419
|
if (!groups.has(key)) {
|
|
6409
|
-
const filePath =
|
|
6420
|
+
const filePath = path29.join(outputDir, `edges_${fromTable}_${toTable}.csv`);
|
|
6410
6421
|
groups.set(key, { lines: [header], from: fromTable, to: toTable, filePath });
|
|
6411
6422
|
}
|
|
6412
6423
|
groups.get(key).lines.push(
|
|
@@ -6421,7 +6432,7 @@ function writeEdgeCSV(graph, outputDir) {
|
|
|
6421
6432
|
}
|
|
6422
6433
|
const result = [];
|
|
6423
6434
|
for (const group of groups.values()) {
|
|
6424
|
-
|
|
6435
|
+
fs23.writeFileSync(group.filePath, group.lines.join(""), "utf-8");
|
|
6425
6436
|
result.push({ fromTable: group.from, toTable: group.to, filePath: group.filePath });
|
|
6426
6437
|
}
|
|
6427
6438
|
return result;
|
|
@@ -6449,7 +6460,7 @@ async function loadGraphToDB(graph, dbManager) {
|
|
|
6449
6460
|
} catch {
|
|
6450
6461
|
}
|
|
6451
6462
|
}
|
|
6452
|
-
const tmpDir =
|
|
6463
|
+
const tmpDir = fs23.mkdtempSync(path29.join(os12.tmpdir(), "code-intel-csv-"));
|
|
6453
6464
|
try {
|
|
6454
6465
|
const nodeTableFiles = writeNodeCSVs(graph, tmpDir);
|
|
6455
6466
|
const edgeGroups = writeEdgeCSV(graph, tmpDir);
|
|
@@ -6468,8 +6479,8 @@ async function loadGraphToDB(graph, dbManager) {
|
|
|
6468
6479
|
}
|
|
6469
6480
|
let nodeCount = 0;
|
|
6470
6481
|
for (const [table, csvPath] of nodeTableFiles) {
|
|
6471
|
-
if (!
|
|
6472
|
-
const stat =
|
|
6482
|
+
if (!fs23.existsSync(csvPath)) continue;
|
|
6483
|
+
const stat = fs23.statSync(csvPath);
|
|
6473
6484
|
if (stat.size < 50) continue;
|
|
6474
6485
|
try {
|
|
6475
6486
|
await dbManager.execute(
|
|
@@ -6482,8 +6493,8 @@ async function loadGraphToDB(graph, dbManager) {
|
|
|
6482
6493
|
}
|
|
6483
6494
|
let edgeCount = 0;
|
|
6484
6495
|
for (const group of edgeGroups) {
|
|
6485
|
-
if (!
|
|
6486
|
-
const stat =
|
|
6496
|
+
if (!fs23.existsSync(group.filePath)) continue;
|
|
6497
|
+
const stat = fs23.statSync(group.filePath);
|
|
6487
6498
|
if (stat.size < 50) continue;
|
|
6488
6499
|
try {
|
|
6489
6500
|
await dbManager.execute(
|
|
@@ -6497,7 +6508,7 @@ async function loadGraphToDB(graph, dbManager) {
|
|
|
6497
6508
|
return { nodeCount, edgeCount };
|
|
6498
6509
|
} finally {
|
|
6499
6510
|
try {
|
|
6500
|
-
|
|
6511
|
+
fs23.rmSync(tmpDir, { recursive: true, force: true });
|
|
6501
6512
|
} catch {
|
|
6502
6513
|
}
|
|
6503
6514
|
}
|
|
@@ -6549,19 +6560,19 @@ function buildNodeProps(node) {
|
|
|
6549
6560
|
function escCypher(s) {
|
|
6550
6561
|
return s.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/\n/g, "\\n").replace(/\r/g, "");
|
|
6551
6562
|
}
|
|
6552
|
-
var GLOBAL_DIR =
|
|
6553
|
-
var REPOS_FILE =
|
|
6563
|
+
var GLOBAL_DIR = path29.join(os12.homedir(), ".code-intel");
|
|
6564
|
+
var REPOS_FILE = path29.join(GLOBAL_DIR, "repos.json");
|
|
6554
6565
|
function loadRegistry() {
|
|
6555
6566
|
try {
|
|
6556
|
-
const data =
|
|
6567
|
+
const data = fs23.readFileSync(REPOS_FILE, "utf-8");
|
|
6557
6568
|
return JSON.parse(data);
|
|
6558
6569
|
} catch {
|
|
6559
6570
|
return [];
|
|
6560
6571
|
}
|
|
6561
6572
|
}
|
|
6562
6573
|
function saveRegistry(entries) {
|
|
6563
|
-
|
|
6564
|
-
|
|
6574
|
+
fs23.mkdirSync(GLOBAL_DIR, { recursive: true });
|
|
6575
|
+
fs23.writeFileSync(REPOS_FILE, JSON.stringify(entries, null, 2));
|
|
6565
6576
|
}
|
|
6566
6577
|
function upsertRepo(entry) {
|
|
6567
6578
|
const entries = loadRegistry();
|
|
@@ -6578,23 +6589,23 @@ function removeRepo(repoPath) {
|
|
|
6578
6589
|
saveRegistry(entries);
|
|
6579
6590
|
}
|
|
6580
6591
|
function saveMetadata(repoDir, metadata) {
|
|
6581
|
-
const metaDir =
|
|
6582
|
-
|
|
6583
|
-
|
|
6592
|
+
const metaDir = path29.join(repoDir, ".code-intel");
|
|
6593
|
+
fs23.mkdirSync(metaDir, { recursive: true });
|
|
6594
|
+
fs23.writeFileSync(path29.join(metaDir, "meta.json"), JSON.stringify(metadata, null, 2));
|
|
6584
6595
|
}
|
|
6585
6596
|
function loadMetadata(repoDir) {
|
|
6586
6597
|
try {
|
|
6587
|
-
const data =
|
|
6598
|
+
const data = fs23.readFileSync(path29.join(repoDir, ".code-intel", "meta.json"), "utf-8");
|
|
6588
6599
|
return JSON.parse(data);
|
|
6589
6600
|
} catch {
|
|
6590
6601
|
return null;
|
|
6591
6602
|
}
|
|
6592
6603
|
}
|
|
6593
6604
|
function getDbPath(repoDir) {
|
|
6594
|
-
return
|
|
6605
|
+
return path29.join(repoDir, ".code-intel", "graph.db");
|
|
6595
6606
|
}
|
|
6596
6607
|
function getVectorDbPath(repoDir) {
|
|
6597
|
-
return
|
|
6608
|
+
return path29.join(repoDir, ".code-intel", "vector.db");
|
|
6598
6609
|
}
|
|
6599
6610
|
|
|
6600
6611
|
// src/mcp-server/server.ts
|
|
@@ -6672,6 +6683,205 @@ async function loadGraphFromDB(graph, db) {
|
|
|
6672
6683
|
|
|
6673
6684
|
// src/multi-repo/group-sync.ts
|
|
6674
6685
|
init_logger();
|
|
6686
|
+
function scanForFiles(root, matcher, maxDepth = 2) {
|
|
6687
|
+
const results = [];
|
|
6688
|
+
function walk(dir, depth) {
|
|
6689
|
+
if (depth > maxDepth) return;
|
|
6690
|
+
let entries;
|
|
6691
|
+
try {
|
|
6692
|
+
entries = fs23.readdirSync(dir, { withFileTypes: true });
|
|
6693
|
+
} catch {
|
|
6694
|
+
return;
|
|
6695
|
+
}
|
|
6696
|
+
for (const entry of entries) {
|
|
6697
|
+
const full = path29.join(dir, entry.name);
|
|
6698
|
+
if (entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== "node_modules") {
|
|
6699
|
+
walk(full, depth + 1);
|
|
6700
|
+
} else if (entry.isFile() && matcher(entry.name)) {
|
|
6701
|
+
results.push(full);
|
|
6702
|
+
}
|
|
6703
|
+
}
|
|
6704
|
+
}
|
|
6705
|
+
walk(root, 0);
|
|
6706
|
+
return results;
|
|
6707
|
+
}
|
|
6708
|
+
|
|
6709
|
+
// src/multi-repo/schema-parsers/openapi-parser.ts
|
|
6710
|
+
var OPENAPI_FILENAMES = /* @__PURE__ */ new Set([
|
|
6711
|
+
"openapi.yaml",
|
|
6712
|
+
"openapi.json",
|
|
6713
|
+
"openapi.yml",
|
|
6714
|
+
"swagger.yaml",
|
|
6715
|
+
"swagger.json",
|
|
6716
|
+
"swagger.yml"
|
|
6717
|
+
]);
|
|
6718
|
+
var HTTP_METHODS = ["get", "post", "put", "patch", "delete", "head", "options"];
|
|
6719
|
+
function tryParseFile(filePath) {
|
|
6720
|
+
const ext = path29.extname(filePath).toLowerCase();
|
|
6721
|
+
const content = fs23.readFileSync(filePath, "utf-8");
|
|
6722
|
+
if (ext === ".json") {
|
|
6723
|
+
try {
|
|
6724
|
+
return JSON.parse(content);
|
|
6725
|
+
} catch {
|
|
6726
|
+
return null;
|
|
6727
|
+
}
|
|
6728
|
+
}
|
|
6729
|
+
return null;
|
|
6730
|
+
}
|
|
6731
|
+
async function parseOpenAPIContracts(repoRoot) {
|
|
6732
|
+
const files = scanForFiles(repoRoot, (name) => OPENAPI_FILENAMES.has(name));
|
|
6733
|
+
const contracts = [];
|
|
6734
|
+
for (const filePath of files) {
|
|
6735
|
+
const spec = tryParseFile(filePath);
|
|
6736
|
+
if (!spec) continue;
|
|
6737
|
+
const paths = spec["paths"];
|
|
6738
|
+
if (!paths || typeof paths !== "object") continue;
|
|
6739
|
+
for (const [pathStr, pathItem] of Object.entries(paths)) {
|
|
6740
|
+
if (!pathItem || typeof pathItem !== "object") continue;
|
|
6741
|
+
const ops = pathItem;
|
|
6742
|
+
for (const method of HTTP_METHODS) {
|
|
6743
|
+
if (!(method in ops)) continue;
|
|
6744
|
+
const operation = ops[method];
|
|
6745
|
+
if (!operation) continue;
|
|
6746
|
+
const requestBody = operation["requestBody"];
|
|
6747
|
+
const requestSchema = requestBody?.["content"] ? requestBody["content"]["application/json"]?.["schema"] : void 0;
|
|
6748
|
+
const responses = operation["responses"];
|
|
6749
|
+
const ok200 = responses?.["200"];
|
|
6750
|
+
const responseSchema = ok200?.["content"] ? ok200["content"]["application/json"]?.["schema"] : void 0;
|
|
6751
|
+
contracts.push({
|
|
6752
|
+
name: `${method.toUpperCase()} ${pathStr}`,
|
|
6753
|
+
kind: "route",
|
|
6754
|
+
method: method.toUpperCase(),
|
|
6755
|
+
path: pathStr,
|
|
6756
|
+
...requestSchema ? { requestSchema } : {},
|
|
6757
|
+
...responseSchema ? { responseSchema } : {},
|
|
6758
|
+
filePath
|
|
6759
|
+
});
|
|
6760
|
+
}
|
|
6761
|
+
}
|
|
6762
|
+
}
|
|
6763
|
+
return contracts;
|
|
6764
|
+
}
|
|
6765
|
+
function extractFieldNames(block) {
|
|
6766
|
+
return block.split("\n").map((line) => line.trim()).filter((line) => line && !line.startsWith("#")).map((line) => {
|
|
6767
|
+
const m = line.match(/^(\w+)\s*[(:]/);
|
|
6768
|
+
return m ? m[1] : null;
|
|
6769
|
+
}).filter((f) => f !== null);
|
|
6770
|
+
}
|
|
6771
|
+
async function parseGraphQLContracts(repoRoot) {
|
|
6772
|
+
const files = scanForFiles(repoRoot, (name) => name.endsWith(".graphql") || name.endsWith(".gql"));
|
|
6773
|
+
const contracts = [];
|
|
6774
|
+
for (const filePath of files) {
|
|
6775
|
+
const content = fs23.readFileSync(filePath, "utf-8");
|
|
6776
|
+
const typeRegex = /type\s+(\w+)\s*\{([^}]+)\}/g;
|
|
6777
|
+
let match;
|
|
6778
|
+
while ((match = typeRegex.exec(content)) !== null) {
|
|
6779
|
+
const typeName = match[1];
|
|
6780
|
+
const body = match[2];
|
|
6781
|
+
const fields = extractFieldNames(body);
|
|
6782
|
+
const lcName = typeName.toLowerCase();
|
|
6783
|
+
if (lcName === "query") {
|
|
6784
|
+
for (const field of fields) {
|
|
6785
|
+
contracts.push({ name: `query.${field}`, kind: "graphql", operation: "query", fields, filePath });
|
|
6786
|
+
}
|
|
6787
|
+
} else if (lcName === "mutation") {
|
|
6788
|
+
for (const field of fields) {
|
|
6789
|
+
contracts.push({ name: `mutation.${field}`, kind: "graphql", operation: "mutation", fields, filePath });
|
|
6790
|
+
}
|
|
6791
|
+
} else if (lcName === "subscription") {
|
|
6792
|
+
for (const field of fields) {
|
|
6793
|
+
contracts.push({ name: `subscription.${field}`, kind: "graphql", operation: "subscription", fields, filePath });
|
|
6794
|
+
}
|
|
6795
|
+
} else {
|
|
6796
|
+
contracts.push({ name: `type.${typeName}`, kind: "graphql", operation: "type", fields, filePath });
|
|
6797
|
+
}
|
|
6798
|
+
}
|
|
6799
|
+
}
|
|
6800
|
+
return contracts;
|
|
6801
|
+
}
|
|
6802
|
+
async function parseProtoContracts(repoRoot) {
|
|
6803
|
+
const files = scanForFiles(repoRoot, (name) => name.endsWith(".proto"));
|
|
6804
|
+
const contracts = [];
|
|
6805
|
+
for (const filePath of files) {
|
|
6806
|
+
const content = fs23.readFileSync(filePath, "utf-8");
|
|
6807
|
+
const serviceRegex = /service\s+(\w+)\s*\{([^}]+)\}/g;
|
|
6808
|
+
let serviceMatch;
|
|
6809
|
+
while ((serviceMatch = serviceRegex.exec(content)) !== null) {
|
|
6810
|
+
const serviceName = serviceMatch[1];
|
|
6811
|
+
const body = serviceMatch[2];
|
|
6812
|
+
const rpcRegex = /rpc\s+(\w+)\s*\((\w+)\)\s*returns\s*\((\w+)\)/g;
|
|
6813
|
+
let rpcMatch;
|
|
6814
|
+
while ((rpcMatch = rpcRegex.exec(body)) !== null) {
|
|
6815
|
+
const rpcName = rpcMatch[1];
|
|
6816
|
+
const inputType = rpcMatch[2];
|
|
6817
|
+
const outputType = rpcMatch[3];
|
|
6818
|
+
contracts.push({
|
|
6819
|
+
name: `${serviceName}.${rpcName}`,
|
|
6820
|
+
kind: "grpc",
|
|
6821
|
+
serviceName,
|
|
6822
|
+
rpcName,
|
|
6823
|
+
inputType,
|
|
6824
|
+
outputType,
|
|
6825
|
+
filePath
|
|
6826
|
+
});
|
|
6827
|
+
}
|
|
6828
|
+
}
|
|
6829
|
+
}
|
|
6830
|
+
return contracts;
|
|
6831
|
+
}
|
|
6832
|
+
|
|
6833
|
+
// src/multi-repo/type-similarity.ts
|
|
6834
|
+
function normalizeType(t) {
|
|
6835
|
+
return t.toLowerCase().replace(/[\[\]?<>\s]/g, "").trim();
|
|
6836
|
+
}
|
|
6837
|
+
function paramTypeSimilarity(paramsA, paramsB) {
|
|
6838
|
+
if (paramsA.length === 0 && paramsB.length === 0) return 1;
|
|
6839
|
+
if (paramsA.length === 0 || paramsB.length === 0) return 0;
|
|
6840
|
+
const setA = new Set(paramsA.map((p) => normalizeType(p.type ?? "")).filter(Boolean));
|
|
6841
|
+
const setB = new Set(paramsB.map((p) => normalizeType(p.type ?? "")).filter(Boolean));
|
|
6842
|
+
if (setA.size === 0 && setB.size === 0) return 1;
|
|
6843
|
+
if (setA.size === 0 || setB.size === 0) return 0;
|
|
6844
|
+
const intersection = [...setA].filter((x) => setB.has(x)).length;
|
|
6845
|
+
const union = (/* @__PURE__ */ new Set([...setA, ...setB])).size;
|
|
6846
|
+
return intersection / union;
|
|
6847
|
+
}
|
|
6848
|
+
function returnTypeSimilarity(typeA, typeB) {
|
|
6849
|
+
if (!typeA || !typeB) return 0.5;
|
|
6850
|
+
const a = normalizeType(typeA);
|
|
6851
|
+
const b = normalizeType(typeB);
|
|
6852
|
+
if (a === b) return 1;
|
|
6853
|
+
const compatible = [
|
|
6854
|
+
["string", "str"],
|
|
6855
|
+
["number", "int"],
|
|
6856
|
+
["number", "float"],
|
|
6857
|
+
["number", "double"],
|
|
6858
|
+
["boolean", "bool"],
|
|
6859
|
+
["void", "unit"],
|
|
6860
|
+
["void", "none"]
|
|
6861
|
+
];
|
|
6862
|
+
for (const [x, y] of compatible) {
|
|
6863
|
+
if (a === x && b === y || a === y && b === x) return 0.8;
|
|
6864
|
+
}
|
|
6865
|
+
return 0;
|
|
6866
|
+
}
|
|
6867
|
+
function paramCountSimilarity(countA, countB) {
|
|
6868
|
+
const maxCount = Math.max(countA, countB, 1);
|
|
6869
|
+
return 1 - Math.abs(countA - countB) / maxCount;
|
|
6870
|
+
}
|
|
6871
|
+
function computeContractSimilarity(a, b, nameSim) {
|
|
6872
|
+
const paramsA = a.parameters ?? [];
|
|
6873
|
+
const paramsB = b.parameters ?? [];
|
|
6874
|
+
const ptSim = paramTypeSimilarity(paramsA, paramsB);
|
|
6875
|
+
const rtSim = returnTypeSimilarity(a.returnType, b.returnType);
|
|
6876
|
+
const pcSim = paramCountSimilarity(paramsA.length, paramsB.length);
|
|
6877
|
+
let score = 0.4 * nameSim + 0.3 * ptSim + 0.2 * rtSim + 0.1 * pcSim;
|
|
6878
|
+
if (ptSim > 0.8) {
|
|
6879
|
+
score = Math.min(1, score * 1.2);
|
|
6880
|
+
}
|
|
6881
|
+
return score;
|
|
6882
|
+
}
|
|
6883
|
+
|
|
6884
|
+
// src/multi-repo/group-sync.ts
|
|
6675
6885
|
function extractContracts(graph, repoName, repoPath) {
|
|
6676
6886
|
const contracts = [];
|
|
6677
6887
|
for (const node of graph.allNodes()) {
|
|
@@ -6684,7 +6894,10 @@ function extractContracts(graph, repoName, repoPath) {
|
|
|
6684
6894
|
nodeId: node.id,
|
|
6685
6895
|
nodeKind: node.kind,
|
|
6686
6896
|
filePath: node.filePath,
|
|
6687
|
-
signature: node.content?.split("\n")[0]?.trim()
|
|
6897
|
+
signature: node.content?.split("\n")[0]?.trim(),
|
|
6898
|
+
parameters: node.metadata?.parameters ?? node.metadata?.params,
|
|
6899
|
+
returnType: node.metadata?.returnType,
|
|
6900
|
+
exported: node.exported
|
|
6688
6901
|
});
|
|
6689
6902
|
}
|
|
6690
6903
|
if (node.kind === "route") {
|
|
@@ -6746,13 +6959,15 @@ function matchContracts(allContracts) {
|
|
|
6746
6959
|
const consumer = consumerByName.get(provider.name);
|
|
6747
6960
|
if (consumer) {
|
|
6748
6961
|
const sameKind = provider.kind === consumer.kind;
|
|
6962
|
+
const typedScore = computeContractSimilarity(provider, consumer, 1);
|
|
6963
|
+
const confidence = sameKind ? Math.max(typedScore, 0.9) : Math.max(typedScore, 0.6);
|
|
6749
6964
|
links.push({
|
|
6750
6965
|
providerRepo: provider.repoName,
|
|
6751
6966
|
providerContract: provider.name,
|
|
6752
6967
|
consumerRepo: consumer.repoName,
|
|
6753
6968
|
consumerContract: consumer.name,
|
|
6754
6969
|
matchKind: provider.kind === "route" ? "route-match" : "name-match",
|
|
6755
|
-
confidence:
|
|
6970
|
+
confidence: Math.min(1, confidence)
|
|
6756
6971
|
});
|
|
6757
6972
|
} else {
|
|
6758
6973
|
const providerLC = provider.name.toLowerCase();
|
|
@@ -6793,8 +7008,8 @@ async function syncGroup(group) {
|
|
|
6793
7008
|
logger_default.warn(` \u26A0 Registry entry "${member.registryName}" not found \u2014 skipping ${member.groupPath}`);
|
|
6794
7009
|
continue;
|
|
6795
7010
|
}
|
|
6796
|
-
const dbPath =
|
|
6797
|
-
if (!
|
|
7011
|
+
const dbPath = path29.join(regEntry.path, ".code-intel", "graph.db");
|
|
7012
|
+
if (!fs23.existsSync(dbPath)) {
|
|
6798
7013
|
logger_default.warn(` \u26A0 No index at ${dbPath} \u2014 run \`code-intel analyze ${regEntry.path}\` first`);
|
|
6799
7014
|
continue;
|
|
6800
7015
|
}
|
|
@@ -6810,6 +7025,44 @@ async function syncGroup(group) {
|
|
|
6810
7025
|
continue;
|
|
6811
7026
|
}
|
|
6812
7027
|
const contracts = extractContracts(graph, member.registryName, regEntry.path);
|
|
7028
|
+
const [openapiContracts, graphqlContracts, protoContracts] = await Promise.all([
|
|
7029
|
+
parseOpenAPIContracts(regEntry.path).catch(() => []),
|
|
7030
|
+
parseGraphQLContracts(regEntry.path).catch(() => []),
|
|
7031
|
+
parseProtoContracts(regEntry.path).catch(() => [])
|
|
7032
|
+
]);
|
|
7033
|
+
for (const c of openapiContracts) {
|
|
7034
|
+
contracts.push({
|
|
7035
|
+
repoName: member.registryName,
|
|
7036
|
+
repoPath: regEntry.path,
|
|
7037
|
+
kind: "route",
|
|
7038
|
+
name: c.name,
|
|
7039
|
+
nodeId: `openapi:${c.method}:${c.path}`,
|
|
7040
|
+
nodeKind: "route",
|
|
7041
|
+
filePath: c.filePath
|
|
7042
|
+
});
|
|
7043
|
+
}
|
|
7044
|
+
for (const c of graphqlContracts) {
|
|
7045
|
+
contracts.push({
|
|
7046
|
+
repoName: member.registryName,
|
|
7047
|
+
repoPath: regEntry.path,
|
|
7048
|
+
kind: "graphql",
|
|
7049
|
+
name: c.name,
|
|
7050
|
+
nodeId: `graphql:${c.name}`,
|
|
7051
|
+
nodeKind: "graphql",
|
|
7052
|
+
filePath: c.filePath
|
|
7053
|
+
});
|
|
7054
|
+
}
|
|
7055
|
+
for (const c of protoContracts) {
|
|
7056
|
+
contracts.push({
|
|
7057
|
+
repoName: member.registryName,
|
|
7058
|
+
repoPath: regEntry.path,
|
|
7059
|
+
kind: "grpc",
|
|
7060
|
+
name: c.name,
|
|
7061
|
+
nodeId: `grpc:${c.serviceName}:${c.rpcName}`,
|
|
7062
|
+
nodeKind: "grpc",
|
|
7063
|
+
filePath: c.filePath
|
|
7064
|
+
});
|
|
7065
|
+
}
|
|
6813
7066
|
logger_default.info(` \u2713 ${member.registryName} (${member.groupPath}): ${contracts.length} contracts`);
|
|
6814
7067
|
allContracts.push(...contracts);
|
|
6815
7068
|
}
|
|
@@ -6829,8 +7082,8 @@ async function queryGroup(group, query, limit = 20) {
|
|
|
6829
7082
|
for (const member of group.members) {
|
|
6830
7083
|
const regEntry = registry.find((r) => r.name === member.registryName);
|
|
6831
7084
|
if (!regEntry) continue;
|
|
6832
|
-
const dbPath =
|
|
6833
|
-
if (!
|
|
7085
|
+
const dbPath = path29.join(regEntry.path, ".code-intel", "graph.db");
|
|
7086
|
+
if (!fs23.existsSync(dbPath)) continue;
|
|
6834
7087
|
const graph = createKnowledgeGraph();
|
|
6835
7088
|
const db = new DbManager(dbPath);
|
|
6836
7089
|
try {
|
|
@@ -6860,6 +7113,624 @@ async function queryGroup(group, query, limit = 20) {
|
|
|
6860
7113
|
|
|
6861
7114
|
// src/mcp-server/server.ts
|
|
6862
7115
|
init_tracing();
|
|
7116
|
+
|
|
7117
|
+
// src/query/explain-relationship.ts
|
|
7118
|
+
function explainRelationship(graph, from, to) {
|
|
7119
|
+
const allNodes = [...graph.allNodes()];
|
|
7120
|
+
const fromNode = allNodes.find((n) => n.name === from);
|
|
7121
|
+
if (!fromNode) {
|
|
7122
|
+
const firstChar = from[0]?.toLowerCase() ?? "";
|
|
7123
|
+
const fromLower = from.toLowerCase();
|
|
7124
|
+
const suggestions = allNodes.filter((n) => n.name.toLowerCase().startsWith(firstChar) || n.name.toLowerCase().includes(fromLower)).slice(0, 5).map((n) => n.name);
|
|
7125
|
+
return { error: `Symbol not found: ${from}`, suggestions };
|
|
7126
|
+
}
|
|
7127
|
+
const toNode = allNodes.find((n) => n.name === to);
|
|
7128
|
+
if (!toNode) {
|
|
7129
|
+
const firstChar = to[0]?.toLowerCase() ?? "";
|
|
7130
|
+
const toLower = to.toLowerCase();
|
|
7131
|
+
const suggestions = allNodes.filter((n) => n.name.toLowerCase().startsWith(firstChar) || n.name.toLowerCase().includes(toLower)).slice(0, 5).map((n) => n.name);
|
|
7132
|
+
return { error: `Symbol not found: ${to}`, suggestions };
|
|
7133
|
+
}
|
|
7134
|
+
const paths = [];
|
|
7135
|
+
const queue = [{
|
|
7136
|
+
id: fromNode.id,
|
|
7137
|
+
nodeNames: [fromNode.name],
|
|
7138
|
+
lastEdgeKind: "",
|
|
7139
|
+
visited: /* @__PURE__ */ new Set([fromNode.id])
|
|
7140
|
+
}];
|
|
7141
|
+
while (queue.length > 0 && paths.length < 10) {
|
|
7142
|
+
const entry = queue.shift();
|
|
7143
|
+
const { id, nodeNames, visited } = entry;
|
|
7144
|
+
if (nodeNames.length > 6) continue;
|
|
7145
|
+
for (const edge of graph.findEdgesFrom(id)) {
|
|
7146
|
+
const targetNode = graph.getNode(edge.target);
|
|
7147
|
+
if (!targetNode) continue;
|
|
7148
|
+
if (visited.has(edge.target)) continue;
|
|
7149
|
+
const newNames = [...nodeNames, targetNode.name];
|
|
7150
|
+
if (edge.target === toNode.id) {
|
|
7151
|
+
paths.push({ hops: newNames.length - 1, nodes: newNames, edgeKind: edge.kind });
|
|
7152
|
+
if (paths.length >= 10) break;
|
|
7153
|
+
continue;
|
|
7154
|
+
}
|
|
7155
|
+
if (newNames.length < 6) {
|
|
7156
|
+
const newVisited = new Set(visited);
|
|
7157
|
+
newVisited.add(edge.target);
|
|
7158
|
+
queue.push({ id: edge.target, nodeNames: newNames, lastEdgeKind: edge.kind, visited: newVisited });
|
|
7159
|
+
}
|
|
7160
|
+
}
|
|
7161
|
+
}
|
|
7162
|
+
const fromImports = /* @__PURE__ */ new Set();
|
|
7163
|
+
for (const edge of graph.findEdgesFrom(fromNode.id)) {
|
|
7164
|
+
if (edge.kind === "imports") fromImports.add(edge.target);
|
|
7165
|
+
}
|
|
7166
|
+
const sharedImportIds = [];
|
|
7167
|
+
for (const edge of graph.findEdgesFrom(toNode.id)) {
|
|
7168
|
+
if (edge.kind === "imports" && fromImports.has(edge.target)) {
|
|
7169
|
+
sharedImportIds.push(edge.target);
|
|
7170
|
+
}
|
|
7171
|
+
}
|
|
7172
|
+
const sharedImports = sharedImportIds.map((id) => graph.getNode(id)?.name ?? id);
|
|
7173
|
+
let heritage = null;
|
|
7174
|
+
for (const edge of graph.findEdgesFrom(fromNode.id)) {
|
|
7175
|
+
if ((edge.kind === "extends" || edge.kind === "implements") && edge.target === toNode.id) {
|
|
7176
|
+
heritage = `${from} ${edge.kind} ${to}`;
|
|
7177
|
+
break;
|
|
7178
|
+
}
|
|
7179
|
+
}
|
|
7180
|
+
if (!heritage) {
|
|
7181
|
+
for (const edge of graph.findEdgesFrom(toNode.id)) {
|
|
7182
|
+
if ((edge.kind === "extends" || edge.kind === "implements") && edge.target === fromNode.id) {
|
|
7183
|
+
heritage = `${to} ${edge.kind} ${from}`;
|
|
7184
|
+
break;
|
|
7185
|
+
}
|
|
7186
|
+
}
|
|
7187
|
+
}
|
|
7188
|
+
const sharedStr = sharedImports.length > 0 ? sharedImports.join(", ") : "none";
|
|
7189
|
+
const heritageStr = heritage ?? "none";
|
|
7190
|
+
const connectionStr = paths.length === 0 ? "No connection found." : `${from} \u2192 ${to} via ${paths.length} path(s).`;
|
|
7191
|
+
const summary = `${connectionStr} Shared imports: [${sharedStr}]. Heritage: ${heritageStr}.`;
|
|
7192
|
+
return { paths, sharedImports, heritage, summary };
|
|
7193
|
+
}
|
|
7194
|
+
|
|
7195
|
+
// src/query/pr-impact.ts
|
|
7196
|
+
function parseDiffFiles(diff) {
|
|
7197
|
+
const files = [];
|
|
7198
|
+
for (const line of diff.split("\n")) {
|
|
7199
|
+
const match = line.match(/^\+\+\+ b\/(.+)/);
|
|
7200
|
+
if (match) {
|
|
7201
|
+
files.push(match[1]);
|
|
7202
|
+
}
|
|
7203
|
+
}
|
|
7204
|
+
return files;
|
|
7205
|
+
}
|
|
7206
|
+
function computePRImpact(graph, changedFiles, maxHops) {
|
|
7207
|
+
const changedSymbolIds = /* @__PURE__ */ new Set();
|
|
7208
|
+
for (const node of graph.allNodes()) {
|
|
7209
|
+
if (!node.filePath) continue;
|
|
7210
|
+
for (const changedFile of changedFiles) {
|
|
7211
|
+
if (node.filePath === changedFile || node.filePath.endsWith(changedFile) || changedFile.endsWith(node.filePath)) {
|
|
7212
|
+
changedSymbolIds.add(node.id);
|
|
7213
|
+
break;
|
|
7214
|
+
}
|
|
7215
|
+
}
|
|
7216
|
+
}
|
|
7217
|
+
const allBlastRadiusNodes = /* @__PURE__ */ new Set();
|
|
7218
|
+
const changedSymbols = [];
|
|
7219
|
+
for (const symbolId of changedSymbolIds) {
|
|
7220
|
+
const symbolNode = graph.getNode(symbolId);
|
|
7221
|
+
if (!symbolNode) continue;
|
|
7222
|
+
const blastRadius = /* @__PURE__ */ new Set();
|
|
7223
|
+
const queue = [{ id: symbolId, depth: 0 }];
|
|
7224
|
+
const visited = /* @__PURE__ */ new Set();
|
|
7225
|
+
while (queue.length > 0) {
|
|
7226
|
+
const { id, depth } = queue.shift();
|
|
7227
|
+
if (visited.has(id) || depth > maxHops) continue;
|
|
7228
|
+
visited.add(id);
|
|
7229
|
+
if (id !== symbolId) blastRadius.add(id);
|
|
7230
|
+
for (const edge of graph.findEdgesTo(id)) {
|
|
7231
|
+
if (edge.kind === "calls" || edge.kind === "imports") {
|
|
7232
|
+
queue.push({ id: edge.source, depth: depth + 1 });
|
|
7233
|
+
}
|
|
7234
|
+
}
|
|
7235
|
+
}
|
|
7236
|
+
for (const id of blastRadius) allBlastRadiusNodes.add(id);
|
|
7237
|
+
const blastCount = blastRadius.size;
|
|
7238
|
+
let risk;
|
|
7239
|
+
if (blastCount > 50) {
|
|
7240
|
+
risk = "HIGH";
|
|
7241
|
+
} else if (blastCount >= 10) {
|
|
7242
|
+
risk = "MEDIUM";
|
|
7243
|
+
} else {
|
|
7244
|
+
risk = "LOW";
|
|
7245
|
+
}
|
|
7246
|
+
let callerCount = 0;
|
|
7247
|
+
for (const edge of graph.findEdgesTo(symbolId)) {
|
|
7248
|
+
if (edge.kind === "calls") callerCount++;
|
|
7249
|
+
}
|
|
7250
|
+
let testCoverage = false;
|
|
7251
|
+
for (const edge of graph.findEdgesTo(symbolId)) {
|
|
7252
|
+
if (edge.kind === "imports") {
|
|
7253
|
+
const callerNode = graph.getNode(edge.source);
|
|
7254
|
+
if (callerNode?.filePath && (callerNode.filePath.includes(".test.") || callerNode.filePath.includes(".spec."))) {
|
|
7255
|
+
testCoverage = true;
|
|
7256
|
+
break;
|
|
7257
|
+
}
|
|
7258
|
+
}
|
|
7259
|
+
}
|
|
7260
|
+
changedSymbols.push({ name: symbolNode.name, risk, callerCount, testCoverage });
|
|
7261
|
+
}
|
|
7262
|
+
const impactedSymbols = [];
|
|
7263
|
+
for (const id of allBlastRadiusNodes) {
|
|
7264
|
+
if (changedSymbolIds.has(id)) continue;
|
|
7265
|
+
const node = graph.getNode(id);
|
|
7266
|
+
if (node) {
|
|
7267
|
+
impactedSymbols.push({ name: node.name, filePath: node.filePath });
|
|
7268
|
+
}
|
|
7269
|
+
}
|
|
7270
|
+
const riskSummary = { HIGH: 0, MEDIUM: 0, LOW: 0 };
|
|
7271
|
+
for (const s of changedSymbols) {
|
|
7272
|
+
riskSummary[s.risk]++;
|
|
7273
|
+
}
|
|
7274
|
+
const coverageGaps = [];
|
|
7275
|
+
for (const s of changedSymbols) {
|
|
7276
|
+
if ((s.risk === "HIGH" || s.risk === "MEDIUM") && !s.testCoverage) {
|
|
7277
|
+
coverageGaps.push(`${s.name} has no test coverage`);
|
|
7278
|
+
}
|
|
7279
|
+
}
|
|
7280
|
+
const fileImpactCount = /* @__PURE__ */ new Map();
|
|
7281
|
+
for (const sym of impactedSymbols) {
|
|
7282
|
+
if (sym.filePath) {
|
|
7283
|
+
fileImpactCount.set(sym.filePath, (fileImpactCount.get(sym.filePath) ?? 0) + 1);
|
|
7284
|
+
}
|
|
7285
|
+
}
|
|
7286
|
+
const filesToReview = [...fileImpactCount.entries()].sort((a, b) => b[1] - a[1]).slice(0, 5).map(([fp]) => fp);
|
|
7287
|
+
return {
|
|
7288
|
+
changedSymbols,
|
|
7289
|
+
impactedSymbols,
|
|
7290
|
+
riskSummary,
|
|
7291
|
+
coverageGaps,
|
|
7292
|
+
filesToReview,
|
|
7293
|
+
crossRepoImpact: null
|
|
7294
|
+
};
|
|
7295
|
+
}
|
|
7296
|
+
|
|
7297
|
+
// src/query/similar-symbols.ts
|
|
7298
|
+
function levenshtein(a, b) {
|
|
7299
|
+
const m = a.length;
|
|
7300
|
+
const n = b.length;
|
|
7301
|
+
const dp = Array.from(
|
|
7302
|
+
{ length: m + 1 },
|
|
7303
|
+
(_, i) => Array.from({ length: n + 1 }, (_2, j) => i === 0 ? j : j === 0 ? i : 0)
|
|
7304
|
+
);
|
|
7305
|
+
for (let i = 1; i <= m; i++) {
|
|
7306
|
+
for (let j = 1; j <= n; j++) {
|
|
7307
|
+
if (a[i - 1] === b[j - 1]) {
|
|
7308
|
+
dp[i][j] = dp[i - 1][j - 1];
|
|
7309
|
+
} else {
|
|
7310
|
+
dp[i][j] = 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
|
|
7311
|
+
}
|
|
7312
|
+
}
|
|
7313
|
+
}
|
|
7314
|
+
return dp[m][n];
|
|
7315
|
+
}
|
|
7316
|
+
function findSimilarSymbols(graph, symbolName, limit) {
|
|
7317
|
+
const clampedLimit = Math.min(Math.max(1, limit), 50);
|
|
7318
|
+
const allNodes = [...graph.allNodes()];
|
|
7319
|
+
const targetNode = allNodes.find((n) => n.name === symbolName);
|
|
7320
|
+
if (!targetNode) {
|
|
7321
|
+
return { similar: [] };
|
|
7322
|
+
}
|
|
7323
|
+
let targetCluster = null;
|
|
7324
|
+
for (const edge of graph.findEdgesFrom(targetNode.id)) {
|
|
7325
|
+
if (edge.kind === "belongs_to") {
|
|
7326
|
+
const clusterNode = graph.getNode(edge.target);
|
|
7327
|
+
if (clusterNode) {
|
|
7328
|
+
targetCluster = clusterNode.name;
|
|
7329
|
+
break;
|
|
7330
|
+
}
|
|
7331
|
+
}
|
|
7332
|
+
}
|
|
7333
|
+
if (!targetCluster) {
|
|
7334
|
+
for (const edge of graph.findEdgesTo(targetNode.id)) {
|
|
7335
|
+
if (edge.kind === "belongs_to") {
|
|
7336
|
+
const clusterNode = graph.getNode(edge.source);
|
|
7337
|
+
if (clusterNode) {
|
|
7338
|
+
targetCluster = clusterNode.name;
|
|
7339
|
+
break;
|
|
7340
|
+
}
|
|
7341
|
+
}
|
|
7342
|
+
}
|
|
7343
|
+
}
|
|
7344
|
+
const results = [];
|
|
7345
|
+
for (const node of allNodes) {
|
|
7346
|
+
if (node.id === targetNode.id) continue;
|
|
7347
|
+
const maxLen = Math.max(symbolName.length, node.name.length);
|
|
7348
|
+
const nameSim = maxLen === 0 ? 1 : 1 - levenshtein(symbolName, node.name) / maxLen;
|
|
7349
|
+
const structuralSim = node.kind === targetNode.kind ? 0.5 : 0;
|
|
7350
|
+
const combined = 0.5 * nameSim + 0.5 * structuralSim;
|
|
7351
|
+
const reasons = [];
|
|
7352
|
+
if (nameSim >= 0.6) reasons.push("similar name");
|
|
7353
|
+
if (node.kind === targetNode.kind) reasons.push("same kind");
|
|
7354
|
+
if (targetCluster !== null) {
|
|
7355
|
+
let nodeCluster = null;
|
|
7356
|
+
for (const edge of graph.findEdgesFrom(node.id)) {
|
|
7357
|
+
if (edge.kind === "belongs_to") {
|
|
7358
|
+
const clusterNode = graph.getNode(edge.target);
|
|
7359
|
+
if (clusterNode) {
|
|
7360
|
+
nodeCluster = clusterNode.name;
|
|
7361
|
+
break;
|
|
7362
|
+
}
|
|
7363
|
+
}
|
|
7364
|
+
}
|
|
7365
|
+
if (!nodeCluster) {
|
|
7366
|
+
for (const edge of graph.findEdgesTo(node.id)) {
|
|
7367
|
+
if (edge.kind === "belongs_to") {
|
|
7368
|
+
const clusterNode = graph.getNode(edge.source);
|
|
7369
|
+
if (clusterNode) {
|
|
7370
|
+
nodeCluster = clusterNode.name;
|
|
7371
|
+
break;
|
|
7372
|
+
}
|
|
7373
|
+
}
|
|
7374
|
+
}
|
|
7375
|
+
}
|
|
7376
|
+
if (nodeCluster !== null && nodeCluster === targetCluster) {
|
|
7377
|
+
reasons.push("same module");
|
|
7378
|
+
}
|
|
7379
|
+
}
|
|
7380
|
+
if (targetNode.metadata?.["cluster"] !== void 0 && node.metadata?.["cluster"] !== void 0 && node.metadata["cluster"] === targetNode.metadata["cluster"]) {
|
|
7381
|
+
if (!reasons.includes("same module")) reasons.push("same module");
|
|
7382
|
+
}
|
|
7383
|
+
results.push({ name: node.name, similarity: combined, reasons });
|
|
7384
|
+
}
|
|
7385
|
+
results.sort((a, b) => b.similarity - a.similarity);
|
|
7386
|
+
return { similar: results.slice(0, clampedLimit) };
|
|
7387
|
+
}
|
|
7388
|
+
|
|
7389
|
+
// src/query/health-report.ts
|
|
7390
|
+
function computeHealthReport(graph, scope) {
|
|
7391
|
+
const wholeRepo = scope === ".";
|
|
7392
|
+
function inScope(filePath) {
|
|
7393
|
+
if (wholeRepo) return true;
|
|
7394
|
+
return filePath.startsWith(scope) || filePath.includes(scope);
|
|
7395
|
+
}
|
|
7396
|
+
const scopedNodes = [...graph.allNodes()].filter((n) => inScope(n.filePath));
|
|
7397
|
+
const deadCodeKinds = /* @__PURE__ */ new Set(["function", "method", "class"]);
|
|
7398
|
+
const deadCode = [];
|
|
7399
|
+
for (const node of scopedNodes) {
|
|
7400
|
+
if (!deadCodeKinds.has(node.kind)) continue;
|
|
7401
|
+
if (node.exported === true) continue;
|
|
7402
|
+
let hasIncoming = false;
|
|
7403
|
+
for (const _edge of graph.findEdgesTo(node.id)) {
|
|
7404
|
+
hasIncoming = true;
|
|
7405
|
+
break;
|
|
7406
|
+
}
|
|
7407
|
+
if (!hasIncoming) {
|
|
7408
|
+
deadCode.push({ name: node.name, filePath: node.filePath, kind: node.kind });
|
|
7409
|
+
if (deadCode.length >= 20) break;
|
|
7410
|
+
}
|
|
7411
|
+
}
|
|
7412
|
+
const cycles = [];
|
|
7413
|
+
const scopedNodeIds = new Set(scopedNodes.map((n) => n.id));
|
|
7414
|
+
const importAdj = /* @__PURE__ */ new Map();
|
|
7415
|
+
for (const node of scopedNodes) {
|
|
7416
|
+
importAdj.set(node.id, []);
|
|
7417
|
+
}
|
|
7418
|
+
for (const edge of graph.findEdgesByKind("imports")) {
|
|
7419
|
+
if (scopedNodeIds.has(edge.source) && scopedNodeIds.has(edge.target)) {
|
|
7420
|
+
importAdj.get(edge.source).push(edge.target);
|
|
7421
|
+
}
|
|
7422
|
+
}
|
|
7423
|
+
const visited = /* @__PURE__ */ new Set();
|
|
7424
|
+
const inStack = /* @__PURE__ */ new Set();
|
|
7425
|
+
const stackPath = [];
|
|
7426
|
+
function dfs(nodeId) {
|
|
7427
|
+
if (cycles.length >= 5) return;
|
|
7428
|
+
visited.add(nodeId);
|
|
7429
|
+
inStack.add(nodeId);
|
|
7430
|
+
stackPath.push(nodeId);
|
|
7431
|
+
for (const neighborId of importAdj.get(nodeId) ?? []) {
|
|
7432
|
+
if (cycles.length >= 5) break;
|
|
7433
|
+
if (inStack.has(neighborId)) {
|
|
7434
|
+
const cycleStart = stackPath.indexOf(neighborId);
|
|
7435
|
+
const cyclePath = stackPath.slice(cycleStart).map((id) => {
|
|
7436
|
+
const node = graph.getNode(id);
|
|
7437
|
+
return node ? node.name : id;
|
|
7438
|
+
});
|
|
7439
|
+
cycles.push(cyclePath);
|
|
7440
|
+
} else if (!visited.has(neighborId)) {
|
|
7441
|
+
dfs(neighborId);
|
|
7442
|
+
}
|
|
7443
|
+
}
|
|
7444
|
+
stackPath.pop();
|
|
7445
|
+
inStack.delete(nodeId);
|
|
7446
|
+
}
|
|
7447
|
+
for (const node of scopedNodes) {
|
|
7448
|
+
if (cycles.length >= 5) break;
|
|
7449
|
+
if (!visited.has(node.id)) {
|
|
7450
|
+
dfs(node.id);
|
|
7451
|
+
}
|
|
7452
|
+
}
|
|
7453
|
+
const godNodes = [];
|
|
7454
|
+
for (const node of scopedNodes) {
|
|
7455
|
+
let edgeCount = 0;
|
|
7456
|
+
for (const _edge of graph.findEdgesFrom(node.id)) {
|
|
7457
|
+
edgeCount++;
|
|
7458
|
+
}
|
|
7459
|
+
if (edgeCount > 10) {
|
|
7460
|
+
godNodes.push({ name: node.name, edgeCount, filePath: node.filePath });
|
|
7461
|
+
}
|
|
7462
|
+
}
|
|
7463
|
+
godNodes.sort((a, b) => b.edgeCount - a.edgeCount);
|
|
7464
|
+
godNodes.splice(10);
|
|
7465
|
+
const filePathToNodes = /* @__PURE__ */ new Map();
|
|
7466
|
+
for (const node of scopedNodes) {
|
|
7467
|
+
if (!node.filePath) continue;
|
|
7468
|
+
let arr = filePathToNodes.get(node.filePath);
|
|
7469
|
+
if (!arr) {
|
|
7470
|
+
arr = [];
|
|
7471
|
+
filePathToNodes.set(node.filePath, arr);
|
|
7472
|
+
}
|
|
7473
|
+
arr.push(node.id);
|
|
7474
|
+
}
|
|
7475
|
+
const orphanFiles = [];
|
|
7476
|
+
for (const [filePath, nodeIds] of filePathToNodes) {
|
|
7477
|
+
if (orphanFiles.length >= 10) break;
|
|
7478
|
+
let hasAnyEdge = false;
|
|
7479
|
+
for (const nodeId of nodeIds) {
|
|
7480
|
+
let hasOut = false;
|
|
7481
|
+
for (const _edge of graph.findEdgesFrom(nodeId)) {
|
|
7482
|
+
hasOut = true;
|
|
7483
|
+
break;
|
|
7484
|
+
}
|
|
7485
|
+
let hasIn = false;
|
|
7486
|
+
for (const _edge of graph.findEdgesTo(nodeId)) {
|
|
7487
|
+
hasIn = true;
|
|
7488
|
+
break;
|
|
7489
|
+
}
|
|
7490
|
+
if (hasOut || hasIn) {
|
|
7491
|
+
hasAnyEdge = true;
|
|
7492
|
+
break;
|
|
7493
|
+
}
|
|
7494
|
+
}
|
|
7495
|
+
if (!hasAnyEdge) {
|
|
7496
|
+
orphanFiles.push(filePath);
|
|
7497
|
+
}
|
|
7498
|
+
}
|
|
7499
|
+
const hotspotCandidates = [];
|
|
7500
|
+
for (const node of scopedNodes) {
|
|
7501
|
+
const visitedBfs = /* @__PURE__ */ new Set();
|
|
7502
|
+
const queue = [{ id: node.id, depth: 0 }];
|
|
7503
|
+
while (queue.length > 0) {
|
|
7504
|
+
const item = queue.shift();
|
|
7505
|
+
if (item.depth > 5 || visitedBfs.has(item.id)) continue;
|
|
7506
|
+
visitedBfs.add(item.id);
|
|
7507
|
+
for (const edge of graph.findEdgesTo(item.id)) {
|
|
7508
|
+
if (edge.kind === "calls" || edge.kind === "imports") {
|
|
7509
|
+
if (!visitedBfs.has(edge.source)) {
|
|
7510
|
+
queue.push({ id: edge.source, depth: item.depth + 1 });
|
|
7511
|
+
}
|
|
7512
|
+
}
|
|
7513
|
+
}
|
|
7514
|
+
}
|
|
7515
|
+
const blastRadius = visitedBfs.size - 1;
|
|
7516
|
+
hotspotCandidates.push({ name: node.name, blastRadius, filePath: node.filePath });
|
|
7517
|
+
}
|
|
7518
|
+
hotspotCandidates.sort((a, b) => b.blastRadius - a.blastRadius);
|
|
7519
|
+
const complexityHotspots = hotspotCandidates.slice(0, 5);
|
|
7520
|
+
const healthScore = Math.max(
|
|
7521
|
+
0,
|
|
7522
|
+
Math.min(100, 100 - deadCode.length * 2 - cycles.length * 5 - godNodes.length * 3)
|
|
7523
|
+
);
|
|
7524
|
+
return {
|
|
7525
|
+
healthScore,
|
|
7526
|
+
deadCode,
|
|
7527
|
+
cycles,
|
|
7528
|
+
godNodes,
|
|
7529
|
+
orphanFiles,
|
|
7530
|
+
complexityHotspots
|
|
7531
|
+
};
|
|
7532
|
+
}
|
|
7533
|
+
|
|
7534
|
+
// src/query/suggest-tests.ts
|
|
7535
|
+
function getSuggestedCases(symbolName) {
|
|
7536
|
+
const lower = symbolName.toLowerCase();
|
|
7537
|
+
if (/parse|validate|check|verify/.test(lower)) {
|
|
7538
|
+
return [
|
|
7539
|
+
"Valid input \u2192 success",
|
|
7540
|
+
"Invalid input \u2192 throws error",
|
|
7541
|
+
"Edge case: empty/null input \u2192 handled gracefully"
|
|
7542
|
+
];
|
|
7543
|
+
}
|
|
7544
|
+
if (/create|add|insert|save/.test(lower)) {
|
|
7545
|
+
return [
|
|
7546
|
+
"Success: valid data \u2192 created",
|
|
7547
|
+
"Duplicate: existing item \u2192 error or no-op",
|
|
7548
|
+
"Missing required fields \u2192 validation error"
|
|
7549
|
+
];
|
|
7550
|
+
}
|
|
7551
|
+
if (/delete|remove|destroy/.test(lower)) {
|
|
7552
|
+
return [
|
|
7553
|
+
"Existing item \u2192 deleted successfully",
|
|
7554
|
+
"Non-existent item \u2192 no error or 404",
|
|
7555
|
+
"Unauthorized access \u2192 rejected"
|
|
7556
|
+
];
|
|
7557
|
+
}
|
|
7558
|
+
if (/get|find|fetch|load/.test(lower)) {
|
|
7559
|
+
return [
|
|
7560
|
+
"Found: returns correct data",
|
|
7561
|
+
"Not found: returns null or throws",
|
|
7562
|
+
"Empty collection: returns []"
|
|
7563
|
+
];
|
|
7564
|
+
}
|
|
7565
|
+
return [
|
|
7566
|
+
"Happy path: valid input \u2192 expected output",
|
|
7567
|
+
"Error case: invalid input \u2192 error handled",
|
|
7568
|
+
"Edge case: boundary values \u2192 correct behavior"
|
|
7569
|
+
];
|
|
7570
|
+
}
|
|
7571
|
+
function suggestTests(graph, symbolName) {
|
|
7572
|
+
let targetNode = void 0;
|
|
7573
|
+
for (const node of graph.allNodes()) {
|
|
7574
|
+
if (node.name === symbolName) {
|
|
7575
|
+
targetNode = node;
|
|
7576
|
+
break;
|
|
7577
|
+
}
|
|
7578
|
+
}
|
|
7579
|
+
if (!targetNode) {
|
|
7580
|
+
return { error: `Symbol not found: ${symbolName}` };
|
|
7581
|
+
}
|
|
7582
|
+
const targetId = targetNode.id;
|
|
7583
|
+
const callPaths = [];
|
|
7584
|
+
const pathQueue = [{ id: targetId, path: [symbolName], depth: 0 }];
|
|
7585
|
+
while (pathQueue.length > 0 && callPaths.length < 5) {
|
|
7586
|
+
const { id, path: path30, depth } = pathQueue.shift();
|
|
7587
|
+
let hasCallers = false;
|
|
7588
|
+
for (const edge of graph.findEdgesTo(id)) {
|
|
7589
|
+
if (edge.kind !== "calls") continue;
|
|
7590
|
+
const callerNode = graph.getNode(edge.source);
|
|
7591
|
+
if (!callerNode) continue;
|
|
7592
|
+
hasCallers = true;
|
|
7593
|
+
const newPath = [callerNode.name, ...path30];
|
|
7594
|
+
if (depth + 1 >= 3 || callPaths.length >= 5) {
|
|
7595
|
+
if (callPaths.length < 5) callPaths.push(newPath);
|
|
7596
|
+
continue;
|
|
7597
|
+
}
|
|
7598
|
+
pathQueue.push({ id: edge.source, path: newPath, depth: depth + 1 });
|
|
7599
|
+
}
|
|
7600
|
+
if (!hasCallers && path30.length > 1) {
|
|
7601
|
+
callPaths.push(path30);
|
|
7602
|
+
}
|
|
7603
|
+
}
|
|
7604
|
+
if (callPaths.length === 0) {
|
|
7605
|
+
for (const edge of graph.findEdgesTo(targetId)) {
|
|
7606
|
+
if (edge.kind !== "calls") continue;
|
|
7607
|
+
const callerNode = graph.getNode(edge.source);
|
|
7608
|
+
if (!callerNode) continue;
|
|
7609
|
+
callPaths.push([callerNode.name, symbolName]);
|
|
7610
|
+
if (callPaths.length >= 5) break;
|
|
7611
|
+
}
|
|
7612
|
+
}
|
|
7613
|
+
const existingTestFiles = /* @__PURE__ */ new Set();
|
|
7614
|
+
for (const edge of graph.findEdgesTo(targetId)) {
|
|
7615
|
+
if (edge.kind !== "imports") continue;
|
|
7616
|
+
const importerNode = graph.getNode(edge.source);
|
|
7617
|
+
if (!importerNode) continue;
|
|
7618
|
+
if (importerNode.filePath.includes(".test.") || importerNode.filePath.includes(".spec.")) {
|
|
7619
|
+
existingTestFiles.add(importerNode.filePath);
|
|
7620
|
+
}
|
|
7621
|
+
}
|
|
7622
|
+
const existingTests = [...existingTestFiles];
|
|
7623
|
+
const untestedCallers = [];
|
|
7624
|
+
for (const edge of graph.findEdgesTo(targetId)) {
|
|
7625
|
+
if (edge.kind !== "calls") continue;
|
|
7626
|
+
const callerNode = graph.getNode(edge.source);
|
|
7627
|
+
if (!callerNode) continue;
|
|
7628
|
+
if (callerNode.filePath.includes(".test.") || callerNode.filePath.includes(".spec.")) {
|
|
7629
|
+
continue;
|
|
7630
|
+
}
|
|
7631
|
+
let callerHasTest = false;
|
|
7632
|
+
for (const callerImportEdge of graph.findEdgesTo(callerNode.id)) {
|
|
7633
|
+
if (callerImportEdge.kind !== "imports") continue;
|
|
7634
|
+
const importerOfCaller = graph.getNode(callerImportEdge.source);
|
|
7635
|
+
if (!importerOfCaller) continue;
|
|
7636
|
+
if (importerOfCaller.filePath.includes(".test.") || importerOfCaller.filePath.includes(".spec.")) {
|
|
7637
|
+
callerHasTest = true;
|
|
7638
|
+
break;
|
|
7639
|
+
}
|
|
7640
|
+
}
|
|
7641
|
+
if (!callerHasTest) {
|
|
7642
|
+
untestedCallers.push(callerNode.name);
|
|
7643
|
+
}
|
|
7644
|
+
}
|
|
7645
|
+
const suggestedCases = getSuggestedCases(symbolName);
|
|
7646
|
+
return {
|
|
7647
|
+
callPaths,
|
|
7648
|
+
suggestedCases,
|
|
7649
|
+
existingTests,
|
|
7650
|
+
untestedCallers
|
|
7651
|
+
};
|
|
7652
|
+
}
|
|
7653
|
+
|
|
7654
|
+
// src/query/cluster-summary.ts
|
|
7655
|
+
function getPathPrefix(filePath) {
|
|
7656
|
+
const parts = filePath.replace(/\\/g, "/").split("/");
|
|
7657
|
+
return parts.slice(0, 2).join("/");
|
|
7658
|
+
}
|
|
7659
|
+
function summarizeCluster(graph, cluster) {
|
|
7660
|
+
const clusterNodes = [...graph.allNodes()].filter(
|
|
7661
|
+
(n) => n.filePath.startsWith(cluster) || n.metadata?.["cluster"] === cluster
|
|
7662
|
+
);
|
|
7663
|
+
if (clusterNodes.length === 0) {
|
|
7664
|
+
return { error: `Cluster not found: ${cluster}` };
|
|
7665
|
+
}
|
|
7666
|
+
const clusterNodeIds = new Set(clusterNodes.map((n) => n.id));
|
|
7667
|
+
const callerCountMap = /* @__PURE__ */ new Map();
|
|
7668
|
+
for (const node of clusterNodes) {
|
|
7669
|
+
let count = 0;
|
|
7670
|
+
for (const _edge of graph.findEdgesTo(node.id)) {
|
|
7671
|
+
count++;
|
|
7672
|
+
}
|
|
7673
|
+
callerCountMap.set(node.id, count);
|
|
7674
|
+
}
|
|
7675
|
+
const sortedByCallers = [...clusterNodes].sort(
|
|
7676
|
+
(a, b) => (callerCountMap.get(b.id) ?? 0) - (callerCountMap.get(a.id) ?? 0)
|
|
7677
|
+
);
|
|
7678
|
+
const keySymbols = sortedByCallers.slice(0, 5).map((n) => ({
|
|
7679
|
+
name: n.name,
|
|
7680
|
+
callerCount: callerCountMap.get(n.id) ?? 0
|
|
7681
|
+
}));
|
|
7682
|
+
const depsSet = /* @__PURE__ */ new Set();
|
|
7683
|
+
for (const node of clusterNodes) {
|
|
7684
|
+
for (const edge of graph.findEdgesFrom(node.id)) {
|
|
7685
|
+
if (edge.kind !== "imports") continue;
|
|
7686
|
+
const targetNode = graph.getNode(edge.target);
|
|
7687
|
+
if (!targetNode) continue;
|
|
7688
|
+
if (!clusterNodeIds.has(targetNode.id)) {
|
|
7689
|
+
const prefix = getPathPrefix(targetNode.filePath);
|
|
7690
|
+
depsSet.add(prefix);
|
|
7691
|
+
}
|
|
7692
|
+
}
|
|
7693
|
+
}
|
|
7694
|
+
const dependencies = [...depsSet];
|
|
7695
|
+
const dependentsSet = /* @__PURE__ */ new Set();
|
|
7696
|
+
for (const node of clusterNodes) {
|
|
7697
|
+
for (const edge of graph.findEdgesTo(node.id)) {
|
|
7698
|
+
if (edge.kind !== "imports") continue;
|
|
7699
|
+
const sourceNode = graph.getNode(edge.source);
|
|
7700
|
+
if (!sourceNode) continue;
|
|
7701
|
+
if (!clusterNodeIds.has(sourceNode.id)) {
|
|
7702
|
+
const prefix = getPathPrefix(sourceNode.filePath);
|
|
7703
|
+
dependentsSet.add(prefix);
|
|
7704
|
+
}
|
|
7705
|
+
}
|
|
7706
|
+
}
|
|
7707
|
+
const dependents = [...dependentsSet];
|
|
7708
|
+
const healthResult = computeHealthReport(graph, cluster);
|
|
7709
|
+
const health = { score: healthResult.healthScore };
|
|
7710
|
+
const symbolCount = {};
|
|
7711
|
+
for (const node of clusterNodes) {
|
|
7712
|
+
symbolCount[node.kind] = (symbolCount[node.kind] ?? 0) + 1;
|
|
7713
|
+
}
|
|
7714
|
+
let purpose;
|
|
7715
|
+
const topNode = sortedByCallers[0];
|
|
7716
|
+
if (topNode?.metadata?.["summary"] && typeof topNode.metadata["summary"] === "string") {
|
|
7717
|
+
purpose = topNode.metadata["summary"];
|
|
7718
|
+
} else {
|
|
7719
|
+
const clusterName = cluster.split("/").pop() ?? cluster;
|
|
7720
|
+
purpose = `Handles ${clusterName.replace(/[-_/]/g, " ")} functionality`;
|
|
7721
|
+
}
|
|
7722
|
+
return {
|
|
7723
|
+
cluster,
|
|
7724
|
+
purpose,
|
|
7725
|
+
keySymbols,
|
|
7726
|
+
dependencies,
|
|
7727
|
+
dependents,
|
|
7728
|
+
health,
|
|
7729
|
+
symbolCount
|
|
7730
|
+
};
|
|
7731
|
+
}
|
|
7732
|
+
|
|
7733
|
+
// src/mcp-server/server.ts
|
|
6863
7734
|
function createMcpServer(graph, repoName, workspaceRoot) {
|
|
6864
7735
|
const server = new Server(
|
|
6865
7736
|
{ name: "code-intel", version: "0.1.0" },
|
|
@@ -6889,7 +7760,8 @@ function createMcpServer(graph, repoName, workspaceRoot) {
|
|
|
6889
7760
|
type: "object",
|
|
6890
7761
|
properties: {
|
|
6891
7762
|
query: { type: "string", description: "Search query (symbol name, keyword, or partial match)" },
|
|
6892
|
-
|
|
7763
|
+
offset: { type: "number", description: "Number of results to skip for pagination (default: 0)" },
|
|
7764
|
+
limit: { type: "number", description: "Max results per page (default: 50, max: 500)" },
|
|
6893
7765
|
..._tokenProp
|
|
6894
7766
|
},
|
|
6895
7767
|
required: ["query"]
|
|
@@ -6932,6 +7804,8 @@ function createMcpServer(graph, repoName, workspaceRoot) {
|
|
|
6932
7804
|
type: "object",
|
|
6933
7805
|
properties: {
|
|
6934
7806
|
file_path: { type: "string", description: 'File path (partial match is supported, e.g. "auth/login.ts")' },
|
|
7807
|
+
offset: { type: "number", description: "Number of results to skip for pagination (default: 0)" },
|
|
7808
|
+
limit: { type: "number", description: "Max results per page (default: 50, max: 500)" },
|
|
6935
7809
|
..._tokenProp
|
|
6936
7810
|
},
|
|
6937
7811
|
required: ["file_path"]
|
|
@@ -6961,7 +7835,8 @@ function createMcpServer(graph, repoName, workspaceRoot) {
|
|
|
6961
7835
|
type: "string",
|
|
6962
7836
|
description: "Filter by node kind: function | class | interface | method | type_alias | constant | enum (optional)"
|
|
6963
7837
|
},
|
|
6964
|
-
|
|
7838
|
+
offset: { type: "number", description: "Number of results to skip for pagination (default: 0)" },
|
|
7839
|
+
limit: { type: "number", description: "Max results per page (default: 50, max: 500)" },
|
|
6965
7840
|
..._tokenProp
|
|
6966
7841
|
}
|
|
6967
7842
|
}
|
|
@@ -6978,7 +7853,8 @@ function createMcpServer(graph, repoName, workspaceRoot) {
|
|
|
6978
7853
|
inputSchema: {
|
|
6979
7854
|
type: "object",
|
|
6980
7855
|
properties: {
|
|
6981
|
-
|
|
7856
|
+
offset: { type: "number", description: "Number of results to skip for pagination (default: 0)" },
|
|
7857
|
+
limit: { type: "number", description: "Max clusters per page (default: 50, max: 500)" },
|
|
6982
7858
|
..._tokenProp
|
|
6983
7859
|
}
|
|
6984
7860
|
}
|
|
@@ -6989,7 +7865,8 @@ function createMcpServer(graph, repoName, workspaceRoot) {
|
|
|
6989
7865
|
inputSchema: {
|
|
6990
7866
|
type: "object",
|
|
6991
7867
|
properties: {
|
|
6992
|
-
|
|
7868
|
+
offset: { type: "number", description: "Number of results to skip for pagination (default: 0)" },
|
|
7869
|
+
limit: { type: "number", description: "Max flows per page (default: 50, max: 500)" },
|
|
6993
7870
|
..._tokenProp
|
|
6994
7871
|
}
|
|
6995
7872
|
}
|
|
@@ -7111,6 +7988,91 @@ function createMcpServer(graph, repoName, workspaceRoot) {
|
|
|
7111
7988
|
},
|
|
7112
7989
|
required: ["name"]
|
|
7113
7990
|
}
|
|
7991
|
+
},
|
|
7992
|
+
// ── Reasoning / analysis tools ────────────────────────────────────────
|
|
7993
|
+
{
|
|
7994
|
+
name: "explain_relationship",
|
|
7995
|
+
description: "Explain how two symbols are connected: directed paths, shared imports, and heritage (extends/implements). Returns up to 10 paths with at most 5 hops each.",
|
|
7996
|
+
inputSchema: {
|
|
7997
|
+
type: "object",
|
|
7998
|
+
properties: {
|
|
7999
|
+
from: { type: "string", description: "Source symbol name" },
|
|
8000
|
+
to: { type: "string", description: "Target symbol name" },
|
|
8001
|
+
..._tokenProp
|
|
8002
|
+
},
|
|
8003
|
+
required: ["from", "to"]
|
|
8004
|
+
}
|
|
8005
|
+
},
|
|
8006
|
+
{
|
|
8007
|
+
name: "pr_impact",
|
|
8008
|
+
description: "Given changed files or a unified diff, compute full blast radius with risk scores (HIGH/MEDIUM/LOW), test coverage gaps, and top files to review.",
|
|
8009
|
+
inputSchema: {
|
|
8010
|
+
type: "object",
|
|
8011
|
+
properties: {
|
|
8012
|
+
changedFiles: {
|
|
8013
|
+
type: "array",
|
|
8014
|
+
items: { type: "string" },
|
|
8015
|
+
description: "List of changed file paths (relative or absolute)"
|
|
8016
|
+
},
|
|
8017
|
+
diff: {
|
|
8018
|
+
type: "string",
|
|
8019
|
+
description: "Raw unified diff text. Changed files are extracted automatically."
|
|
8020
|
+
},
|
|
8021
|
+
maxHops: {
|
|
8022
|
+
type: "number",
|
|
8023
|
+
description: "Maximum BFS depth for blast radius (default: 5)"
|
|
8024
|
+
},
|
|
8025
|
+
..._tokenProp
|
|
8026
|
+
}
|
|
8027
|
+
}
|
|
8028
|
+
},
|
|
8029
|
+
{
|
|
8030
|
+
name: "similar_symbols",
|
|
8031
|
+
description: "Find symbols with similar names or structure using Levenshtein distance and kind matching. Useful for finding related functions, classes, or interfaces.",
|
|
8032
|
+
inputSchema: {
|
|
8033
|
+
type: "object",
|
|
8034
|
+
properties: {
|
|
8035
|
+
symbol: { type: "string", description: "Symbol name to find similar symbols for" },
|
|
8036
|
+
limit: { type: "number", description: "Maximum number of results (default: 10, max: 50)" },
|
|
8037
|
+
..._tokenProp
|
|
8038
|
+
},
|
|
8039
|
+
required: ["symbol"]
|
|
8040
|
+
}
|
|
8041
|
+
},
|
|
8042
|
+
{
|
|
8043
|
+
name: "health_report",
|
|
8044
|
+
description: "Code health signals for a scope: dead code, cycles, god nodes, orphan files, complexity hotspots",
|
|
8045
|
+
inputSchema: {
|
|
8046
|
+
type: "object",
|
|
8047
|
+
properties: {
|
|
8048
|
+
scope: { type: "string", description: "Directory scope, e.g. 'src/api/' or '.' for whole repo" },
|
|
8049
|
+
..._tokenProp
|
|
8050
|
+
}
|
|
8051
|
+
}
|
|
8052
|
+
},
|
|
8053
|
+
{
|
|
8054
|
+
name: "suggest_tests",
|
|
8055
|
+
description: "Suggest test cases for a symbol: call paths, suggested cases, existing tests, untested callers",
|
|
8056
|
+
inputSchema: {
|
|
8057
|
+
type: "object",
|
|
8058
|
+
properties: {
|
|
8059
|
+
symbol: { type: "string", description: "Symbol name to generate test suggestions for" },
|
|
8060
|
+
..._tokenProp
|
|
8061
|
+
},
|
|
8062
|
+
required: ["symbol"]
|
|
8063
|
+
}
|
|
8064
|
+
},
|
|
8065
|
+
{
|
|
8066
|
+
name: "cluster_summary",
|
|
8067
|
+
description: "Rich summary of a module/cluster: purpose, key symbols, dependencies, health",
|
|
8068
|
+
inputSchema: {
|
|
8069
|
+
type: "object",
|
|
8070
|
+
properties: {
|
|
8071
|
+
cluster: { type: "string", description: "Cluster path e.g. 'src/auth'" },
|
|
8072
|
+
..._tokenProp
|
|
8073
|
+
},
|
|
8074
|
+
required: ["cluster"]
|
|
8075
|
+
}
|
|
7114
8076
|
}
|
|
7115
8077
|
]
|
|
7116
8078
|
}));
|
|
@@ -7181,8 +8143,8 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
|
|
|
7181
8143
|
for (const edge of graph.allEdges()) {
|
|
7182
8144
|
edgeCounts[edge.kind] = (edgeCounts[edge.kind] ?? 0) + 1;
|
|
7183
8145
|
}
|
|
7184
|
-
const { computeHealthReport:
|
|
7185
|
-
const healthReport =
|
|
8146
|
+
const { computeHealthReport: computeHealthReport3 } = await Promise.resolve().then(() => (init_health_score(), health_score_exports));
|
|
8147
|
+
const healthReport = computeHealthReport3(graph);
|
|
7186
8148
|
const health = {
|
|
7187
8149
|
score: Math.round(healthReport.score),
|
|
7188
8150
|
grade: healthReport.grade,
|
|
@@ -7207,10 +8169,37 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
|
|
|
7207
8169
|
// ── search ─────────────────────────────────────────────────────────────
|
|
7208
8170
|
case "search": {
|
|
7209
8171
|
const query = a.query;
|
|
7210
|
-
const
|
|
8172
|
+
const offset = a.offset ?? 0;
|
|
8173
|
+
const effectiveLimit = Math.min(a.limit ?? 50, 500);
|
|
7211
8174
|
const vdbPath = workspaceRoot ? getVectorDbPath(workspaceRoot) : void 0;
|
|
7212
|
-
const
|
|
7213
|
-
|
|
8175
|
+
const fetchLimit = Math.min(offset + effectiveLimit, 500);
|
|
8176
|
+
const { results: allResults, searchMode } = await hybridSearch(graph, query, fetchLimit, { vectorDbPath: vdbPath });
|
|
8177
|
+
const total = allResults.length;
|
|
8178
|
+
const results = allResults.slice(offset, offset + effectiveLimit);
|
|
8179
|
+
const hasMore = offset + effectiveLimit < total;
|
|
8180
|
+
const suggestNextTools = [];
|
|
8181
|
+
const suggestEnabled = process.env["CODE_INTEL_SUGGEST_NEXT_TOOLS"] !== "false";
|
|
8182
|
+
if (suggestEnabled && results.length > 0) {
|
|
8183
|
+
const topName = results[0].name;
|
|
8184
|
+
suggestNextTools.push(
|
|
8185
|
+
{ tool: "inspect", reason: "Inspect the top result in detail", input: { symbol: topName } },
|
|
8186
|
+
{ tool: "similar_symbols", reason: "Find symbols similar to the top result", input: { symbol: topName } }
|
|
8187
|
+
);
|
|
8188
|
+
}
|
|
8189
|
+
return {
|
|
8190
|
+
content: [{
|
|
8191
|
+
type: "text",
|
|
8192
|
+
text: JSON.stringify({
|
|
8193
|
+
results,
|
|
8194
|
+
searchMode,
|
|
8195
|
+
total,
|
|
8196
|
+
offset,
|
|
8197
|
+
limit: effectiveLimit,
|
|
8198
|
+
hasMore,
|
|
8199
|
+
...suggestEnabled ? { suggested_next_tools: suggestNextTools } : {}
|
|
8200
|
+
}, null, 2)
|
|
8201
|
+
}]
|
|
8202
|
+
};
|
|
7214
8203
|
}
|
|
7215
8204
|
// ── inspect ────────────────────────────────────────────────────────────
|
|
7216
8205
|
case "inspect": {
|
|
@@ -7219,6 +8208,26 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
|
|
|
7219
8208
|
if (!node) return { content: [{ type: "text", text: `Symbol "${symbolName}" not found. Try search first.` }] };
|
|
7220
8209
|
const incoming = [...graph.findEdgesTo(node.id)];
|
|
7221
8210
|
const outgoing = [...graph.findEdgesFrom(node.id)];
|
|
8211
|
+
const callers = incoming.filter((e) => e.kind === "calls").map((e) => ({
|
|
8212
|
+
id: e.source,
|
|
8213
|
+
name: graph.getNode(e.source)?.name,
|
|
8214
|
+
file: graph.getNode(e.source)?.filePath
|
|
8215
|
+
}));
|
|
8216
|
+
const callees = outgoing.filter((e) => e.kind === "calls").map((e) => ({
|
|
8217
|
+
id: e.target,
|
|
8218
|
+
name: graph.getNode(e.target)?.name,
|
|
8219
|
+
file: graph.getNode(e.target)?.filePath
|
|
8220
|
+
}));
|
|
8221
|
+
const cluster = incoming.filter((e) => e.kind === "belongs_to").map((e) => graph.getNode(e.target)?.name)[0];
|
|
8222
|
+
const suggestEnabled = process.env["CODE_INTEL_SUGGEST_NEXT_TOOLS"] !== "false";
|
|
8223
|
+
const suggestNextTools = [];
|
|
8224
|
+
if (suggestEnabled) {
|
|
8225
|
+
const topCallerName = callers[0]?.name;
|
|
8226
|
+
suggestNextTools.push(
|
|
8227
|
+
...topCallerName ? [{ tool: "explain_relationship", reason: "Explain connection to a related symbol", input: { from: node.name, to: topCallerName } }] : [],
|
|
8228
|
+
...cluster ? [{ tool: "cluster_summary", reason: "Summarize the module this symbol belongs to", input: { cluster } }] : [{ tool: "cluster_summary", reason: "Summarize the module this symbol belongs to", input: { cluster: node.filePath } }]
|
|
8229
|
+
);
|
|
8230
|
+
}
|
|
7222
8231
|
return {
|
|
7223
8232
|
content: [{
|
|
7224
8233
|
type: "text",
|
|
@@ -7232,16 +8241,8 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
|
|
|
7232
8241
|
endLine: node.endLine,
|
|
7233
8242
|
exported: node.exported
|
|
7234
8243
|
},
|
|
7235
|
-
callers
|
|
7236
|
-
|
|
7237
|
-
name: graph.getNode(e.source)?.name,
|
|
7238
|
-
file: graph.getNode(e.source)?.filePath
|
|
7239
|
-
})),
|
|
7240
|
-
callees: outgoing.filter((e) => e.kind === "calls").map((e) => ({
|
|
7241
|
-
id: e.target,
|
|
7242
|
-
name: graph.getNode(e.target)?.name,
|
|
7243
|
-
file: graph.getNode(e.target)?.filePath
|
|
7244
|
-
})),
|
|
8244
|
+
callers,
|
|
8245
|
+
callees,
|
|
7245
8246
|
imports: incoming.filter((e) => e.kind === "imports").map((e) => graph.getNode(e.source)?.name),
|
|
7246
8247
|
importedBy: outgoing.filter((e) => e.kind === "imports").map((e) => graph.getNode(e.target)?.name),
|
|
7247
8248
|
extends: outgoing.filter((e) => e.kind === "extends").map((e) => graph.getNode(e.target)?.name),
|
|
@@ -7250,8 +8251,9 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
|
|
|
7250
8251
|
name: graph.getNode(e.target)?.name,
|
|
7251
8252
|
kind: graph.getNode(e.target)?.kind
|
|
7252
8253
|
})),
|
|
7253
|
-
cluster
|
|
7254
|
-
content: node.content?.slice(0, 500)
|
|
8254
|
+
cluster,
|
|
8255
|
+
content: node.content?.slice(0, 500),
|
|
8256
|
+
...suggestEnabled ? { suggested_next_tools: suggestNextTools } : {}
|
|
7255
8257
|
}, null, 2)
|
|
7256
8258
|
}]
|
|
7257
8259
|
};
|
|
@@ -7287,6 +8289,16 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
|
|
|
7287
8289
|
return n ? { id, name: n.name, kind: n.kind, filePath: n.filePath } : { id };
|
|
7288
8290
|
});
|
|
7289
8291
|
const risk = affected.size > 10 ? "HIGH" : affected.size > 5 ? "MEDIUM" : "LOW";
|
|
8292
|
+
const suggestEnabled = process.env["CODE_INTEL_SUGGEST_NEXT_TOOLS"] !== "false";
|
|
8293
|
+
const suggestNextTools = [];
|
|
8294
|
+
if (suggestEnabled) {
|
|
8295
|
+
const highestRiskSymbol = node.name;
|
|
8296
|
+
const firstFilePath = affectedDetails[0]?.filePath ?? "";
|
|
8297
|
+
suggestNextTools.push(
|
|
8298
|
+
{ tool: "suggest_tests", reason: "Generate tests for the highest-risk symbol", input: { symbol: highestRiskSymbol } },
|
|
8299
|
+
{ tool: "pr_impact", reason: "Compute full PR impact for changed files", input: { changedFiles: [firstFilePath] } }
|
|
8300
|
+
);
|
|
8301
|
+
}
|
|
7290
8302
|
return {
|
|
7291
8303
|
content: [{
|
|
7292
8304
|
type: "text",
|
|
@@ -7294,7 +8306,8 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
|
|
|
7294
8306
|
target: node.name,
|
|
7295
8307
|
affectedCount: affected.size,
|
|
7296
8308
|
riskLevel: risk,
|
|
7297
|
-
affected: affectedDetails
|
|
8309
|
+
affected: affectedDetails,
|
|
8310
|
+
...suggestEnabled ? { suggested_next_tools: suggestNextTools } : {}
|
|
7298
8311
|
}, null, 2)
|
|
7299
8312
|
}]
|
|
7300
8313
|
};
|
|
@@ -7302,17 +8315,27 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
|
|
|
7302
8315
|
// ── file_symbols ───────────────────────────────────────────────────────
|
|
7303
8316
|
case "file_symbols": {
|
|
7304
8317
|
const filePath = a.file_path;
|
|
7305
|
-
const
|
|
8318
|
+
const offset = a.offset ?? 0;
|
|
8319
|
+
const effectiveLimit = Math.min(a.limit ?? 50, 500);
|
|
8320
|
+
const allMatches = [];
|
|
7306
8321
|
for (const node of graph.allNodes()) {
|
|
7307
8322
|
if (node.filePath && node.filePath.includes(filePath)) {
|
|
7308
|
-
|
|
8323
|
+
allMatches.push({ kind: node.kind, name: node.name, startLine: node.startLine, exported: node.exported });
|
|
7309
8324
|
}
|
|
7310
8325
|
}
|
|
7311
|
-
if (
|
|
8326
|
+
if (allMatches.length === 0) {
|
|
7312
8327
|
return { content: [{ type: "text", text: `No symbols found for file path matching "${filePath}".` }] };
|
|
7313
8328
|
}
|
|
7314
|
-
|
|
7315
|
-
|
|
8329
|
+
allMatches.sort((a2, b) => (a2.startLine ?? 0) - (b.startLine ?? 0));
|
|
8330
|
+
const total = allMatches.length;
|
|
8331
|
+
const matches = allMatches.slice(offset, offset + effectiveLimit);
|
|
8332
|
+
const hasMore = offset + effectiveLimit < total;
|
|
8333
|
+
return {
|
|
8334
|
+
content: [{
|
|
8335
|
+
type: "text",
|
|
8336
|
+
text: JSON.stringify({ symbols: matches, total, offset, limit: effectiveLimit, hasMore }, null, 2)
|
|
8337
|
+
}]
|
|
8338
|
+
};
|
|
7316
8339
|
}
|
|
7317
8340
|
// ── find_path ──────────────────────────────────────────────────────────
|
|
7318
8341
|
case "find_path": {
|
|
@@ -7358,15 +8381,23 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
|
|
|
7358
8381
|
// ── list_exports ───────────────────────────────────────────────────────
|
|
7359
8382
|
case "list_exports": {
|
|
7360
8383
|
const kindFilter = a.kind;
|
|
7361
|
-
const
|
|
7362
|
-
const
|
|
8384
|
+
const offset = a.offset ?? 0;
|
|
8385
|
+
const effectiveLimit = Math.min(a.limit ?? 50, 500);
|
|
8386
|
+
const allExports = [];
|
|
7363
8387
|
for (const node of graph.allNodes()) {
|
|
7364
8388
|
if (!node.exported) continue;
|
|
7365
8389
|
if (kindFilter && node.kind !== kindFilter) continue;
|
|
7366
|
-
|
|
7367
|
-
if (exports$1.length >= limit) break;
|
|
8390
|
+
allExports.push({ kind: node.kind, name: node.name, filePath: node.filePath, startLine: node.startLine });
|
|
7368
8391
|
}
|
|
7369
|
-
|
|
8392
|
+
const total = allExports.length;
|
|
8393
|
+
const exports$1 = allExports.slice(offset, offset + effectiveLimit);
|
|
8394
|
+
const hasMore = offset + effectiveLimit < total;
|
|
8395
|
+
return {
|
|
8396
|
+
content: [{
|
|
8397
|
+
type: "text",
|
|
8398
|
+
text: JSON.stringify({ exports: exports$1, total, offset, limit: effectiveLimit, hasMore }, null, 2)
|
|
8399
|
+
}]
|
|
8400
|
+
};
|
|
7370
8401
|
}
|
|
7371
8402
|
// ── routes ─────────────────────────────────────────────────────────────
|
|
7372
8403
|
case "routes": {
|
|
@@ -7380,8 +8411,9 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
|
|
|
7380
8411
|
}
|
|
7381
8412
|
// ── clusters ───────────────────────────────────────────────────────────
|
|
7382
8413
|
case "clusters": {
|
|
7383
|
-
const
|
|
7384
|
-
const
|
|
8414
|
+
const offset = a.offset ?? 0;
|
|
8415
|
+
const effectiveLimit = Math.min(a.limit ?? 50, 500);
|
|
8416
|
+
const allClusters = [];
|
|
7385
8417
|
for (const node of graph.allNodes()) {
|
|
7386
8418
|
if (node.kind === "cluster") {
|
|
7387
8419
|
const members = [];
|
|
@@ -7393,35 +8425,50 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
|
|
|
7393
8425
|
}
|
|
7394
8426
|
}
|
|
7395
8427
|
}
|
|
7396
|
-
|
|
8428
|
+
allClusters.push({
|
|
7397
8429
|
id: node.id,
|
|
7398
8430
|
name: node.name,
|
|
7399
8431
|
memberCount: node.metadata?.memberCount ?? members.length,
|
|
7400
8432
|
topSymbols: members.slice(0, 10)
|
|
7401
8433
|
});
|
|
7402
|
-
if (clusters.length >= limit) break;
|
|
7403
8434
|
}
|
|
7404
8435
|
}
|
|
7405
|
-
|
|
8436
|
+
const total = allClusters.length;
|
|
8437
|
+
const clusters = allClusters.slice(offset, offset + effectiveLimit);
|
|
8438
|
+
const hasMore = offset + effectiveLimit < total;
|
|
8439
|
+
return {
|
|
8440
|
+
content: [{
|
|
8441
|
+
type: "text",
|
|
8442
|
+
text: JSON.stringify({ clusters, total, offset, limit: effectiveLimit, hasMore }, null, 2)
|
|
8443
|
+
}]
|
|
8444
|
+
};
|
|
7406
8445
|
}
|
|
7407
8446
|
// ── flows ──────────────────────────────────────────────────────────────
|
|
7408
8447
|
case "flows": {
|
|
7409
|
-
const
|
|
7410
|
-
const
|
|
8448
|
+
const offset = a.offset ?? 0;
|
|
8449
|
+
const effectiveLimit = Math.min(a.limit ?? 50, 500);
|
|
8450
|
+
const allFlows = [];
|
|
7411
8451
|
for (const node of graph.allNodes()) {
|
|
7412
8452
|
if (node.kind === "flow") {
|
|
7413
8453
|
const steps = node.metadata?.steps;
|
|
7414
|
-
|
|
8454
|
+
allFlows.push({
|
|
7415
8455
|
id: node.id,
|
|
7416
8456
|
name: node.name,
|
|
7417
8457
|
entryPoint: node.metadata?.entryPoint,
|
|
7418
8458
|
steps: steps ?? [],
|
|
7419
8459
|
stepCount: Array.isArray(steps) ? steps.length : 0
|
|
7420
8460
|
});
|
|
7421
|
-
if (flows.length >= limit) break;
|
|
7422
8461
|
}
|
|
7423
8462
|
}
|
|
7424
|
-
|
|
8463
|
+
const total = allFlows.length;
|
|
8464
|
+
const flows = allFlows.slice(offset, offset + effectiveLimit);
|
|
8465
|
+
const hasMore = offset + effectiveLimit < total;
|
|
8466
|
+
return {
|
|
8467
|
+
content: [{
|
|
8468
|
+
type: "text",
|
|
8469
|
+
text: JSON.stringify({ flows, total, offset, limit: effectiveLimit, hasMore }, null, 2)
|
|
8470
|
+
}]
|
|
8471
|
+
};
|
|
7425
8472
|
}
|
|
7426
8473
|
// ── detect_changes ─────────────────────────────────────────────────────
|
|
7427
8474
|
case "detect_changes": {
|
|
@@ -7449,7 +8496,7 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
|
|
|
7449
8496
|
for (const { filePath: changedFile, changedLines } of changedFiles) {
|
|
7450
8497
|
for (const node of graph.allNodes()) {
|
|
7451
8498
|
if (!node.filePath) continue;
|
|
7452
|
-
const normNode = node.filePath.replace(repoRoot + "/", "").replace(repoRoot +
|
|
8499
|
+
const normNode = node.filePath.replace(repoRoot + "/", "").replace(repoRoot + path29.sep, "");
|
|
7453
8500
|
const normChanged = changedFile.replace(/^a\/|^b\//, "");
|
|
7454
8501
|
if (!normNode.endsWith(normChanged) && !normChanged.endsWith(normNode)) continue;
|
|
7455
8502
|
if (node.startLine !== void 0 && node.endLine !== void 0) {
|
|
@@ -7671,6 +8718,57 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
|
|
|
7671
8718
|
}]
|
|
7672
8719
|
};
|
|
7673
8720
|
}
|
|
8721
|
+
// ── explain_relationship ───────────────────────────────────────────────
|
|
8722
|
+
case "explain_relationship": {
|
|
8723
|
+
const fromName = a.from;
|
|
8724
|
+
const toName = a.to;
|
|
8725
|
+
const result = explainRelationship(graph, fromName, toName);
|
|
8726
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
8727
|
+
}
|
|
8728
|
+
// ── pr_impact ──────────────────────────────────────────────────────────
|
|
8729
|
+
case "pr_impact": {
|
|
8730
|
+
const maxHops = a.maxHops ?? 5;
|
|
8731
|
+
let changedFiles = a.changedFiles ?? [];
|
|
8732
|
+
if (a.diff && typeof a.diff === "string") {
|
|
8733
|
+
const diffFiles = parseDiffFiles(a.diff);
|
|
8734
|
+
changedFiles = [.../* @__PURE__ */ new Set([...changedFiles, ...diffFiles])];
|
|
8735
|
+
}
|
|
8736
|
+
if (changedFiles.length === 0) {
|
|
8737
|
+
return {
|
|
8738
|
+
content: [{
|
|
8739
|
+
type: "text",
|
|
8740
|
+
text: JSON.stringify({ error: 'No changed files provided. Supply "changedFiles" or "diff".' })
|
|
8741
|
+
}]
|
|
8742
|
+
};
|
|
8743
|
+
}
|
|
8744
|
+
const result = computePRImpact(graph, changedFiles, maxHops);
|
|
8745
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
8746
|
+
}
|
|
8747
|
+
// ── similar_symbols ────────────────────────────────────────────────────
|
|
8748
|
+
case "similar_symbols": {
|
|
8749
|
+
const symbolName = a.symbol;
|
|
8750
|
+
const limit = a.limit ?? 10;
|
|
8751
|
+
const result = findSimilarSymbols(graph, symbolName, limit);
|
|
8752
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
8753
|
+
}
|
|
8754
|
+
// ── health_report ──────────────────────────────────────────────────────
|
|
8755
|
+
case "health_report": {
|
|
8756
|
+
const scope = a.scope ?? ".";
|
|
8757
|
+
const result = computeHealthReport(graph, scope);
|
|
8758
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
8759
|
+
}
|
|
8760
|
+
// ── suggest_tests ──────────────────────────────────────────────────────
|
|
8761
|
+
case "suggest_tests": {
|
|
8762
|
+
const sym = a.symbol;
|
|
8763
|
+
const result = suggestTests(graph, sym);
|
|
8764
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
8765
|
+
}
|
|
8766
|
+
// ── cluster_summary ────────────────────────────────────────────────────
|
|
8767
|
+
case "cluster_summary": {
|
|
8768
|
+
const cluster = a.cluster;
|
|
8769
|
+
const result = summarizeCluster(graph, cluster);
|
|
8770
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
8771
|
+
}
|
|
7674
8772
|
default:
|
|
7675
8773
|
return { content: [{ type: "text", text: `Unknown tool: ${name}` }] };
|
|
7676
8774
|
}
|
|
@@ -7764,7 +8862,7 @@ var STUCK_THRESHOLD_MINUTES = 30;
|
|
|
7764
8862
|
var JobsDB = class {
|
|
7765
8863
|
db;
|
|
7766
8864
|
constructor(dbPath) {
|
|
7767
|
-
|
|
8865
|
+
fs23.mkdirSync(path29.dirname(dbPath), { recursive: true });
|
|
7768
8866
|
this.db = new Database3(dbPath);
|
|
7769
8867
|
this.db.pragma("journal_mode = WAL");
|
|
7770
8868
|
this.db.pragma("foreign_keys = ON");
|
|
@@ -7906,7 +9004,7 @@ var JobsDB = class {
|
|
|
7906
9004
|
}
|
|
7907
9005
|
};
|
|
7908
9006
|
function getJobsDBPath() {
|
|
7909
|
-
return
|
|
9007
|
+
return path29.join(os12.homedir(), ".code-intel", "jobs.db");
|
|
7910
9008
|
}
|
|
7911
9009
|
var _jobsDB = null;
|
|
7912
9010
|
function getOrCreateJobsDB() {
|
|
@@ -7998,7 +9096,7 @@ var BACKUP_VERSION = "1.0";
|
|
|
7998
9096
|
var ALGORITHM = "aes-256-gcm";
|
|
7999
9097
|
var IV_LENGTH = 16;
|
|
8000
9098
|
function getBackupDir() {
|
|
8001
|
-
return
|
|
9099
|
+
return path29.join(os12.homedir(), ".code-intel", "backups");
|
|
8002
9100
|
}
|
|
8003
9101
|
function getBackupKey() {
|
|
8004
9102
|
const keyHex = process.env["CODE_INTEL_BACKUP_KEY"];
|
|
@@ -8029,30 +9127,30 @@ var BackupService = class {
|
|
|
8029
9127
|
constructor(backupDir) {
|
|
8030
9128
|
this.backupDir = backupDir ?? getBackupDir();
|
|
8031
9129
|
this.key = getBackupKey();
|
|
8032
|
-
|
|
9130
|
+
fs23.mkdirSync(this.backupDir, { recursive: true });
|
|
8033
9131
|
}
|
|
8034
9132
|
/**
|
|
8035
9133
|
* Create a backup for a repository.
|
|
8036
9134
|
* Returns the backup entry.
|
|
8037
9135
|
*/
|
|
8038
9136
|
createBackup(repoPath) {
|
|
8039
|
-
const codeIntelDir =
|
|
9137
|
+
const codeIntelDir = path29.join(repoPath, ".code-intel");
|
|
8040
9138
|
const id = v4();
|
|
8041
9139
|
const createdAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
8042
9140
|
const filesToBackup = [];
|
|
8043
9141
|
const candidates = ["graph.db", "vector.db", "meta.json"];
|
|
8044
9142
|
for (const f of candidates) {
|
|
8045
|
-
const fp =
|
|
8046
|
-
if (
|
|
9143
|
+
const fp = path29.join(codeIntelDir, f);
|
|
9144
|
+
if (fs23.existsSync(fp)) {
|
|
8047
9145
|
filesToBackup.push({ name: f, localPath: fp });
|
|
8048
9146
|
}
|
|
8049
9147
|
}
|
|
8050
|
-
const registryPath =
|
|
8051
|
-
if (
|
|
9148
|
+
const registryPath = path29.join(os12.homedir(), ".code-intel", "registry.json");
|
|
9149
|
+
if (fs23.existsSync(registryPath)) {
|
|
8052
9150
|
filesToBackup.push({ name: "registry.json", localPath: registryPath });
|
|
8053
9151
|
}
|
|
8054
|
-
const usersDbPath =
|
|
8055
|
-
if (
|
|
9152
|
+
const usersDbPath = path29.join(os12.homedir(), ".code-intel", "users.db");
|
|
9153
|
+
if (fs23.existsSync(usersDbPath)) {
|
|
8056
9154
|
filesToBackup.push({ name: "users.db", localPath: usersDbPath });
|
|
8057
9155
|
}
|
|
8058
9156
|
if (filesToBackup.length === 0) {
|
|
@@ -8063,7 +9161,7 @@ var BackupService = class {
|
|
|
8063
9161
|
createdAt,
|
|
8064
9162
|
version: BACKUP_VERSION,
|
|
8065
9163
|
files: filesToBackup.map((f) => {
|
|
8066
|
-
const data =
|
|
9164
|
+
const data = fs23.readFileSync(f.localPath);
|
|
8067
9165
|
return {
|
|
8068
9166
|
name: f.name,
|
|
8069
9167
|
sha256: crypto5.createHash("sha256").update(data).digest("hex"),
|
|
@@ -8077,7 +9175,7 @@ var BackupService = class {
|
|
|
8077
9175
|
manifestLenBuf.writeUInt32BE(manifestBuf.length, 0);
|
|
8078
9176
|
parts.push(manifestLenBuf, manifestBuf);
|
|
8079
9177
|
for (const f of filesToBackup) {
|
|
8080
|
-
const data =
|
|
9178
|
+
const data = fs23.readFileSync(f.localPath);
|
|
8081
9179
|
const nameBuf = Buffer.from(f.name, "utf-8");
|
|
8082
9180
|
const nameLenBuf = Buffer.alloc(2);
|
|
8083
9181
|
nameLenBuf.writeUInt16BE(nameBuf.length, 0);
|
|
@@ -8088,8 +9186,8 @@ var BackupService = class {
|
|
|
8088
9186
|
const plaintext = Buffer.concat(parts);
|
|
8089
9187
|
const encrypted = encryptBuffer(plaintext, this.key);
|
|
8090
9188
|
const backupFileName = `backup-${id}.cib`;
|
|
8091
|
-
const backupPath =
|
|
8092
|
-
|
|
9189
|
+
const backupPath = path29.join(this.backupDir, backupFileName);
|
|
9190
|
+
fs23.writeFileSync(backupPath, encrypted);
|
|
8093
9191
|
const entry = {
|
|
8094
9192
|
id,
|
|
8095
9193
|
createdAt,
|
|
@@ -8116,9 +9214,9 @@ var BackupService = class {
|
|
|
8116
9214
|
async uploadToS3(entry) {
|
|
8117
9215
|
const cfg = getS3Config();
|
|
8118
9216
|
if (!cfg) throw new Error("S3 not configured. Set CODE_INTEL_BACKUP_S3_BUCKET, CODE_INTEL_BACKUP_S3_ACCESS_KEY_ID, CODE_INTEL_BACKUP_S3_SECRET_ACCESS_KEY.");
|
|
8119
|
-
const fileName =
|
|
9217
|
+
const fileName = path29.basename(entry.path);
|
|
8120
9218
|
const s3Key = `${cfg.prefix}${fileName}`;
|
|
8121
|
-
const body =
|
|
9219
|
+
const body = fs23.readFileSync(entry.path);
|
|
8122
9220
|
const result = await s3Request({ method: "PUT", cfg, key: s3Key, body });
|
|
8123
9221
|
if (result.statusCode < 200 || result.statusCode >= 300) {
|
|
8124
9222
|
throw new Error(`S3 upload failed (HTTP ${result.statusCode}): ${result.body.slice(0, 200)}`);
|
|
@@ -8135,8 +9233,8 @@ var BackupService = class {
|
|
|
8135
9233
|
if (result.statusCode < 200 || result.statusCode >= 300) {
|
|
8136
9234
|
throw new Error(`S3 download failed (HTTP ${result.statusCode}): ${result.body.slice(0, 200)}`);
|
|
8137
9235
|
}
|
|
8138
|
-
|
|
8139
|
-
|
|
9236
|
+
fs23.mkdirSync(path29.dirname(destPath), { recursive: true });
|
|
9237
|
+
fs23.writeFileSync(destPath, Buffer.from(result.body, "binary"));
|
|
8140
9238
|
}
|
|
8141
9239
|
/**
|
|
8142
9240
|
* List backup objects in S3 with the configured prefix.
|
|
@@ -8182,10 +9280,10 @@ var BackupService = class {
|
|
|
8182
9280
|
if (!entry) {
|
|
8183
9281
|
throw new Error(`Backup "${backupId}" not found.`);
|
|
8184
9282
|
}
|
|
8185
|
-
if (!
|
|
9283
|
+
if (!fs23.existsSync(entry.path)) {
|
|
8186
9284
|
throw new Error(`Backup file not found at: ${entry.path}`);
|
|
8187
9285
|
}
|
|
8188
|
-
const encrypted =
|
|
9286
|
+
const encrypted = fs23.readFileSync(entry.path);
|
|
8189
9287
|
let plaintext;
|
|
8190
9288
|
try {
|
|
8191
9289
|
plaintext = decryptBuffer(encrypted, this.key);
|
|
@@ -8199,8 +9297,8 @@ var BackupService = class {
|
|
|
8199
9297
|
offset += manifestLen;
|
|
8200
9298
|
const manifest = JSON.parse(manifestStr);
|
|
8201
9299
|
const restoreBase = targetRepoPath ?? entry.repoPath;
|
|
8202
|
-
const codeIntelDir =
|
|
8203
|
-
|
|
9300
|
+
const codeIntelDir = path29.join(restoreBase, ".code-intel");
|
|
9301
|
+
fs23.mkdirSync(codeIntelDir, { recursive: true });
|
|
8204
9302
|
for (const fileEntry of manifest.files) {
|
|
8205
9303
|
const nameLen = plaintext.readUInt16BE(offset);
|
|
8206
9304
|
offset += 2;
|
|
@@ -8217,18 +9315,18 @@ var BackupService = class {
|
|
|
8217
9315
|
}
|
|
8218
9316
|
let destPath;
|
|
8219
9317
|
if (name === "registry.json" || name === "users.db") {
|
|
8220
|
-
destPath =
|
|
9318
|
+
destPath = path29.join(os12.homedir(), ".code-intel", name);
|
|
8221
9319
|
} else {
|
|
8222
|
-
destPath =
|
|
9320
|
+
destPath = path29.join(codeIntelDir, name);
|
|
8223
9321
|
}
|
|
8224
|
-
|
|
9322
|
+
fs23.writeFileSync(destPath, data);
|
|
8225
9323
|
}
|
|
8226
9324
|
}
|
|
8227
9325
|
/**
|
|
8228
9326
|
* Apply retention policy: keep N daily, M weekly, L monthly backups.
|
|
8229
9327
|
*/
|
|
8230
9328
|
applyRetention(options = { daily: 7, weekly: 4, monthly: 12 }) {
|
|
8231
|
-
const entries = this._loadIndex().filter((e) =>
|
|
9329
|
+
const entries = this._loadIndex().filter((e) => fs23.existsSync(e.path)).sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
|
|
8232
9330
|
const keep = /* @__PURE__ */ new Set();
|
|
8233
9331
|
const now = /* @__PURE__ */ new Date();
|
|
8234
9332
|
const dailyCutoff = new Date(now);
|
|
@@ -8258,7 +9356,7 @@ var BackupService = class {
|
|
|
8258
9356
|
for (const e of entries) {
|
|
8259
9357
|
if (!keep.has(e.id)) {
|
|
8260
9358
|
try {
|
|
8261
|
-
|
|
9359
|
+
fs23.unlinkSync(e.path);
|
|
8262
9360
|
deleted++;
|
|
8263
9361
|
} catch {
|
|
8264
9362
|
}
|
|
@@ -8270,17 +9368,17 @@ var BackupService = class {
|
|
|
8270
9368
|
}
|
|
8271
9369
|
// ── Index helpers ──────────────────────────────────────────────────────────
|
|
8272
9370
|
_indexPath() {
|
|
8273
|
-
return
|
|
9371
|
+
return path29.join(this.backupDir, "index.json");
|
|
8274
9372
|
}
|
|
8275
9373
|
_loadIndex() {
|
|
8276
9374
|
try {
|
|
8277
|
-
return JSON.parse(
|
|
9375
|
+
return JSON.parse(fs23.readFileSync(this._indexPath(), "utf-8"));
|
|
8278
9376
|
} catch {
|
|
8279
9377
|
return [];
|
|
8280
9378
|
}
|
|
8281
9379
|
}
|
|
8282
9380
|
_saveIndex(entries) {
|
|
8283
|
-
|
|
9381
|
+
fs23.writeFileSync(this._indexPath(), JSON.stringify(entries, null, 2));
|
|
8284
9382
|
}
|
|
8285
9383
|
_appendIndex(entry) {
|
|
8286
9384
|
const entries = this._loadIndex();
|
|
@@ -8939,6 +10037,30 @@ var openApiSpec = {
|
|
|
8939
10037
|
}
|
|
8940
10038
|
}
|
|
8941
10039
|
},
|
|
10040
|
+
"/groups/{name}/topology": {
|
|
10041
|
+
get: {
|
|
10042
|
+
tags: ["Groups"],
|
|
10043
|
+
summary: "Get the topology of repos and cross-repo contract edges for a group",
|
|
10044
|
+
parameters: [{ name: "name", in: "path", required: true, schema: { type: "string" } }],
|
|
10045
|
+
responses: {
|
|
10046
|
+
"200": {
|
|
10047
|
+
description: "Repos and cross-repo edges",
|
|
10048
|
+
content: {
|
|
10049
|
+
"application/json": {
|
|
10050
|
+
schema: {
|
|
10051
|
+
type: "object",
|
|
10052
|
+
properties: {
|
|
10053
|
+
repos: { type: "array", items: { type: "object", properties: { name: { type: "string" }, groupPath: { type: "string" }, nodeCount: { type: "integer" }, edgeCount: { type: "integer" } } } },
|
|
10054
|
+
edges: { type: "array", items: { type: "object", properties: { source: { type: "string" }, target: { type: "string" }, contractName: { type: "string" }, confidence: { type: "number" }, kind: { type: "string" } } } }
|
|
10055
|
+
}
|
|
10056
|
+
}
|
|
10057
|
+
}
|
|
10058
|
+
}
|
|
10059
|
+
},
|
|
10060
|
+
"404": { description: "Group not found", content: { "application/json": { schema: { "$ref": "#/components/schemas/ErrorResponse" } } } }
|
|
10061
|
+
}
|
|
10062
|
+
}
|
|
10063
|
+
},
|
|
8942
10064
|
"/query": {
|
|
8943
10065
|
post: {
|
|
8944
10066
|
tags: ["GQL"],
|
|
@@ -9049,11 +10171,11 @@ var openApiSpec = {
|
|
|
9049
10171
|
};
|
|
9050
10172
|
|
|
9051
10173
|
// src/http/app.ts
|
|
9052
|
-
var __dirname$1 =
|
|
10174
|
+
var __dirname$1 = path29.dirname(fileURLToPath(import.meta.url));
|
|
9053
10175
|
var WEB_DIST = (() => {
|
|
9054
|
-
const bundled =
|
|
9055
|
-
if (
|
|
9056
|
-
return
|
|
10176
|
+
const bundled = path29.resolve(__dirname$1, "..", "web");
|
|
10177
|
+
if (fs23.existsSync(bundled)) return bundled;
|
|
10178
|
+
return path29.resolve(__dirname$1, "..", "..", "..", "web", "dist");
|
|
9057
10179
|
})();
|
|
9058
10180
|
function getAllowedOrigins() {
|
|
9059
10181
|
const env = process.env["CODE_INTEL_CORS_ORIGINS"];
|
|
@@ -9584,8 +10706,8 @@ function createApp(graph, repoName, workspaceRoot, watcherState) {
|
|
|
9584
10706
|
const registry = loadRegistry();
|
|
9585
10707
|
const entry = registry.find((r) => r.name === requestedRepo || r.path === requestedRepo);
|
|
9586
10708
|
if (!entry) return null;
|
|
9587
|
-
const dbPath =
|
|
9588
|
-
if (!
|
|
10709
|
+
const dbPath = path29.join(entry.path, ".code-intel", "graph.db");
|
|
10710
|
+
if (!fs23.existsSync(dbPath)) return null;
|
|
9589
10711
|
const repoGraph = createKnowledgeGraph();
|
|
9590
10712
|
const db = new DbManager(dbPath);
|
|
9591
10713
|
try {
|
|
@@ -9672,7 +10794,7 @@ function createApp(graph, repoName, workspaceRoot, watcherState) {
|
|
|
9672
10794
|
return;
|
|
9673
10795
|
}
|
|
9674
10796
|
try {
|
|
9675
|
-
const content =
|
|
10797
|
+
const content = fs23.readFileSync(file_path, "utf-8");
|
|
9676
10798
|
res.json({ content });
|
|
9677
10799
|
} catch {
|
|
9678
10800
|
res.status(404).json({ error: { code: ErrorCodes.NOT_FOUND, message: "File not found" } });
|
|
@@ -9930,8 +11052,8 @@ function createApp(graph, repoName, workspaceRoot, watcherState) {
|
|
|
9930
11052
|
for (const member of group.members) {
|
|
9931
11053
|
const regEntry = registry.find((r) => r.name === member.registryName);
|
|
9932
11054
|
if (!regEntry) continue;
|
|
9933
|
-
const dbPath =
|
|
9934
|
-
if (!
|
|
11055
|
+
const dbPath = path29.join(regEntry.path, ".code-intel", "graph.db");
|
|
11056
|
+
if (!fs23.existsSync(dbPath)) continue;
|
|
9935
11057
|
const db = new DbManager(dbPath);
|
|
9936
11058
|
try {
|
|
9937
11059
|
await db.init();
|
|
@@ -9943,6 +11065,45 @@ function createApp(graph, repoName, workspaceRoot, watcherState) {
|
|
|
9943
11065
|
}
|
|
9944
11066
|
res.json({ nodes: [...mergedGraph.allNodes()], edges: [...mergedGraph.allEdges()] });
|
|
9945
11067
|
});
|
|
11068
|
+
app.get("/api/v1/groups/:name/topology", requireAuth, requireRole("viewer"), async (req, res) => {
|
|
11069
|
+
const groupName = req.params["name"];
|
|
11070
|
+
const group = loadGroup(groupName);
|
|
11071
|
+
if (!group) {
|
|
11072
|
+
res.status(404).json({ error: { code: ErrorCodes.NOT_FOUND, message: "Group not found" } });
|
|
11073
|
+
return;
|
|
11074
|
+
}
|
|
11075
|
+
const syncResult = loadSyncResult(groupName);
|
|
11076
|
+
const registry = loadRegistry();
|
|
11077
|
+
const repos = await Promise.all(group.members.map(async (member) => {
|
|
11078
|
+
const regEntry = registry.find((r) => r.name === member.registryName);
|
|
11079
|
+
let nodeCount = 0;
|
|
11080
|
+
let edgeCount = 0;
|
|
11081
|
+
if (regEntry) {
|
|
11082
|
+
const dbPath = path29.join(regEntry.path, ".code-intel", "graph.db");
|
|
11083
|
+
if (fs23.existsSync(dbPath)) {
|
|
11084
|
+
try {
|
|
11085
|
+
const db = new DbManager(dbPath);
|
|
11086
|
+
await db.init();
|
|
11087
|
+
const g = createKnowledgeGraph();
|
|
11088
|
+
await loadGraphFromDB(g, db);
|
|
11089
|
+
db.close();
|
|
11090
|
+
nodeCount = g.size.nodes;
|
|
11091
|
+
edgeCount = g.size.edges;
|
|
11092
|
+
} catch {
|
|
11093
|
+
}
|
|
11094
|
+
}
|
|
11095
|
+
}
|
|
11096
|
+
return { name: member.registryName, groupPath: member.groupPath, nodeCount, edgeCount };
|
|
11097
|
+
}));
|
|
11098
|
+
const edges = syncResult ? syncResult.links.map((link) => ({
|
|
11099
|
+
source: link.providerRepo,
|
|
11100
|
+
target: link.consumerRepo,
|
|
11101
|
+
contractName: link.providerContract,
|
|
11102
|
+
confidence: link.confidence,
|
|
11103
|
+
kind: "contract"
|
|
11104
|
+
})) : [];
|
|
11105
|
+
res.json({ repos, edges });
|
|
11106
|
+
});
|
|
9946
11107
|
app.get("/api/v1/source", requireAuth, requireRole("viewer"), (req, res) => {
|
|
9947
11108
|
const { file, startLine: startLineStr, endLine: endLineStr } = req.query;
|
|
9948
11109
|
if (!file) {
|
|
@@ -9968,14 +11129,14 @@ function createApp(graph, repoName, workspaceRoot, watcherState) {
|
|
|
9968
11129
|
});
|
|
9969
11130
|
return;
|
|
9970
11131
|
}
|
|
9971
|
-
let rawResolved =
|
|
9972
|
-
if (!
|
|
9973
|
-
rawResolved =
|
|
11132
|
+
let rawResolved = path29.normalize(file);
|
|
11133
|
+
if (!path29.isAbsolute(rawResolved) && workspaceRoot) {
|
|
11134
|
+
rawResolved = path29.join(workspaceRoot, rawResolved);
|
|
9974
11135
|
}
|
|
9975
|
-
const resolvedFile =
|
|
11136
|
+
const resolvedFile = path29.resolve(rawResolved);
|
|
9976
11137
|
function isInsideDir(fileAbs, dir) {
|
|
9977
|
-
const rel =
|
|
9978
|
-
return !rel.startsWith("..") && !
|
|
11138
|
+
const rel = path29.relative(path29.resolve(dir), fileAbs);
|
|
11139
|
+
return !rel.startsWith("..") && !path29.isAbsolute(rel);
|
|
9979
11140
|
}
|
|
9980
11141
|
if (workspaceRoot) {
|
|
9981
11142
|
if (!isInsideDir(resolvedFile, workspaceRoot)) {
|
|
@@ -10012,7 +11173,7 @@ function createApp(graph, repoName, workspaceRoot, watcherState) {
|
|
|
10012
11173
|
}
|
|
10013
11174
|
let fileContent;
|
|
10014
11175
|
try {
|
|
10015
|
-
fileContent =
|
|
11176
|
+
fileContent = fs23.readFileSync(resolvedFile, "utf-8");
|
|
10016
11177
|
} catch {
|
|
10017
11178
|
res.status(404).json({
|
|
10018
11179
|
error: {
|
|
@@ -10043,7 +11204,7 @@ function createApp(graph, repoName, workspaceRoot, watcherState) {
|
|
|
10043
11204
|
const contextStart = Math.max(1, startLine - 20);
|
|
10044
11205
|
const contextEnd = Math.min(lines.length, endLine + 20);
|
|
10045
11206
|
const content = lines.slice(contextStart - 1, contextEnd).join("\n");
|
|
10046
|
-
const ext =
|
|
11207
|
+
const ext = path29.extname(resolvedFile).toLowerCase();
|
|
10047
11208
|
const languageMap = {
|
|
10048
11209
|
".ts": "typescript",
|
|
10049
11210
|
".tsx": "typescript",
|
|
@@ -10178,10 +11339,10 @@ function createApp(graph, repoName, workspaceRoot, watcherState) {
|
|
|
10178
11339
|
res.status(500).json({ error: { code: ErrorCodes.INTERNAL_ERROR, message: err instanceof Error ? err.message : String(err), requestId: req.requestId, timestamp: (/* @__PURE__ */ new Date()).toISOString() } });
|
|
10179
11340
|
}
|
|
10180
11341
|
});
|
|
10181
|
-
if (
|
|
11342
|
+
if (fs23.existsSync(WEB_DIST)) {
|
|
10182
11343
|
app.use(express.static(WEB_DIST));
|
|
10183
11344
|
app.get("/{*path}", (_req, res) => {
|
|
10184
|
-
res.sendFile(
|
|
11345
|
+
res.sendFile(path29.join(WEB_DIST, "index.html"));
|
|
10185
11346
|
});
|
|
10186
11347
|
}
|
|
10187
11348
|
app.use("/admin", requireRole("admin"));
|