@vohongtho.infotech/code-intel 0.4.0 → 0.6.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 +32 -9
- package/dist/cli/main.js +2673 -356
- package/dist/cli/main.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2222 -180
- package/dist/index.js.map +1 -1
- package/dist/web/assets/es-CnPQcqTr.js +10 -0
- package/dist/web/assets/{index-Ds0yq7oU.js → index-j-iO6isa.js} +23 -7
- package/dist/web/assets/index-rprt8Su_.css +2 -0
- package/dist/web/index.html +2 -2
- package/package.json +3 -1
- package/dist/web/assets/index-BcUIJvDD.css +0 -2
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createRequire } from 'module';
|
|
2
2
|
import { fileURLToPath } from 'url';
|
|
3
|
-
import
|
|
3
|
+
import path27 from 'path';
|
|
4
4
|
import fs19, { existsSync } from 'fs';
|
|
5
5
|
import { Parser, Language, Query } from 'web-tree-sitter';
|
|
6
6
|
import { NodeSDK } from '@opentelemetry/sdk-node';
|
|
@@ -26,6 +26,7 @@ import { ListToolsRequestSchema, CallToolRequestSchema, ListResourcesRequestSche
|
|
|
26
26
|
import { Database, Connection } from '@ladybugdb/core';
|
|
27
27
|
import { execSync } from 'child_process';
|
|
28
28
|
import express from 'express';
|
|
29
|
+
import compression from 'compression';
|
|
29
30
|
import cors from 'cors';
|
|
30
31
|
import helmet from 'helmet';
|
|
31
32
|
import cookieParser from 'cookie-parser';
|
|
@@ -125,11 +126,11 @@ var init_shared = __esm({
|
|
|
125
126
|
}
|
|
126
127
|
});
|
|
127
128
|
function findBundledWasmDir() {
|
|
128
|
-
const fileDir =
|
|
129
|
+
const fileDir = path27.dirname(fileURLToPath(import.meta.url));
|
|
129
130
|
const candidates = [
|
|
130
|
-
|
|
131
|
+
path27.join(fileDir, "wasm"),
|
|
131
132
|
// dist/index.js → dist/wasm/
|
|
132
|
-
|
|
133
|
+
path27.join(fileDir, "../wasm")
|
|
133
134
|
// dist/cli/main.js → dist/wasm/
|
|
134
135
|
];
|
|
135
136
|
for (const candidate of candidates) {
|
|
@@ -170,7 +171,7 @@ function wasmPath(lang) {
|
|
|
170
171
|
}
|
|
171
172
|
const bundled = BUNDLED_WASM_MAP[lang];
|
|
172
173
|
if (bundled) {
|
|
173
|
-
const bundledPath =
|
|
174
|
+
const bundledPath = path27.join(_bundledWasmDir, bundled);
|
|
174
175
|
if (existsSync(bundledPath)) return bundledPath;
|
|
175
176
|
}
|
|
176
177
|
return null;
|
|
@@ -1161,7 +1162,7 @@ var init_logger = __esm({
|
|
|
1161
1162
|
};
|
|
1162
1163
|
}
|
|
1163
1164
|
/** Global log directory: ~/.code-intel/logs */
|
|
1164
|
-
static LOG_DIR =
|
|
1165
|
+
static LOG_DIR = path27.join(os12.homedir(), ".code-intel", "logs");
|
|
1165
1166
|
static getLogger() {
|
|
1166
1167
|
if (!_Logger.instance) {
|
|
1167
1168
|
const isProduction = process.env.NODE_ENV === "production";
|
|
@@ -1175,7 +1176,7 @@ var init_logger = __esm({
|
|
|
1175
1176
|
}
|
|
1176
1177
|
transports.push(
|
|
1177
1178
|
new DailyRotateFile({
|
|
1178
|
-
filename:
|
|
1179
|
+
filename: path27.join(_Logger.LOG_DIR, "%DATE%-code-intel.log"),
|
|
1179
1180
|
datePattern: "YYYY-MM-DD",
|
|
1180
1181
|
maxSize: "20m",
|
|
1181
1182
|
maxFiles: "14d"
|
|
@@ -1973,14 +1974,14 @@ var init_parse_phase = __esm({
|
|
|
1973
1974
|
const lang = detectLanguage(filePath);
|
|
1974
1975
|
if (!lang) {
|
|
1975
1976
|
if (context2.verbose) {
|
|
1976
|
-
const relativePath2 =
|
|
1977
|
+
const relativePath2 = path27.relative(context2.workspaceRoot, filePath);
|
|
1977
1978
|
logger_default.info(` [parse] skipped (no parser): ${relativePath2}`);
|
|
1978
1979
|
}
|
|
1979
1980
|
continue;
|
|
1980
1981
|
}
|
|
1981
1982
|
const source = context2.fileCache.get(filePath);
|
|
1982
1983
|
if (!source) continue;
|
|
1983
|
-
const relativePath =
|
|
1984
|
+
const relativePath = path27.relative(context2.workspaceRoot, filePath);
|
|
1984
1985
|
const fileNodeId = generateNodeId("file", relativePath, relativePath);
|
|
1985
1986
|
const fileNode = context2.graph.getNode(fileNodeId);
|
|
1986
1987
|
if (fileNode) {
|
|
@@ -2220,11 +2221,11 @@ var init_resolve_phase = __esm({
|
|
|
2220
2221
|
let heritageEdges = 0;
|
|
2221
2222
|
const fileIndex = /* @__PURE__ */ new Map();
|
|
2222
2223
|
for (const fp of filePaths) {
|
|
2223
|
-
const rel =
|
|
2224
|
+
const rel = path27.relative(workspaceRoot, fp);
|
|
2224
2225
|
fileIndex.set(rel, fp);
|
|
2225
2226
|
const noExt = rel.replace(/\.\w+$/, "");
|
|
2226
2227
|
if (!fileIndex.has(noExt)) fileIndex.set(noExt, fp);
|
|
2227
|
-
const base =
|
|
2228
|
+
const base = path27.basename(rel, path27.extname(rel));
|
|
2228
2229
|
if (!fileIndex.has(base)) fileIndex.set(base, fp);
|
|
2229
2230
|
}
|
|
2230
2231
|
const symbolIndex = /* @__PURE__ */ new Map();
|
|
@@ -2255,7 +2256,7 @@ var init_resolve_phase = __esm({
|
|
|
2255
2256
|
for (const filePath of filePaths) {
|
|
2256
2257
|
const lang = detectLanguage(filePath);
|
|
2257
2258
|
if (!lang) continue;
|
|
2258
|
-
const relativePath =
|
|
2259
|
+
const relativePath = path27.relative(workspaceRoot, filePath);
|
|
2259
2260
|
const fileNodeId = generateNodeId("file", relativePath, relativePath);
|
|
2260
2261
|
const source = fileCache.get(filePath);
|
|
2261
2262
|
if (!source) continue;
|
|
@@ -2268,13 +2269,13 @@ var init_resolve_phase = __esm({
|
|
|
2268
2269
|
let resolvedRelPath = null;
|
|
2269
2270
|
if (cleaned.startsWith(".")) {
|
|
2270
2271
|
const cleanedNoJs = cleaned.replace(/\.(js|jsx)$/, "");
|
|
2271
|
-
const fromDir =
|
|
2272
|
+
const fromDir = path27.dirname(relativePath);
|
|
2272
2273
|
for (const ext of ["", ".ts", ".tsx", ".js", ".jsx", ".py", ".java", ".go", "/index.ts", "/index.js"]) {
|
|
2273
|
-
const candidate =
|
|
2274
|
-
const normalized =
|
|
2274
|
+
const candidate = path27.join(fromDir, cleanedNoJs + ext);
|
|
2275
|
+
const normalized = path27.normalize(candidate);
|
|
2275
2276
|
if (fileIndex.has(normalized)) {
|
|
2276
2277
|
const absPath = fileIndex.get(normalized);
|
|
2277
|
-
resolvedRelPath =
|
|
2278
|
+
resolvedRelPath = path27.relative(workspaceRoot, absPath);
|
|
2278
2279
|
break;
|
|
2279
2280
|
}
|
|
2280
2281
|
}
|
|
@@ -2588,7 +2589,7 @@ __export(group_registry_exports, {
|
|
|
2588
2589
|
saveSyncResult: () => saveSyncResult
|
|
2589
2590
|
});
|
|
2590
2591
|
function groupFile(name) {
|
|
2591
|
-
return
|
|
2592
|
+
return path27.join(GROUPS_DIR, `${name}.json`);
|
|
2592
2593
|
}
|
|
2593
2594
|
function loadGroup(name) {
|
|
2594
2595
|
try {
|
|
@@ -2608,7 +2609,7 @@ function listGroups() {
|
|
|
2608
2609
|
if (!file.endsWith(".json") || file.endsWith(".sync.json")) continue;
|
|
2609
2610
|
try {
|
|
2610
2611
|
const g = JSON.parse(
|
|
2611
|
-
fs19.readFileSync(
|
|
2612
|
+
fs19.readFileSync(path27.join(GROUPS_DIR, file), "utf-8")
|
|
2612
2613
|
);
|
|
2613
2614
|
groups.push(g);
|
|
2614
2615
|
} catch {
|
|
@@ -2624,7 +2625,7 @@ function deleteGroup(name) {
|
|
|
2624
2625
|
} catch {
|
|
2625
2626
|
}
|
|
2626
2627
|
try {
|
|
2627
|
-
fs19.unlinkSync(
|
|
2628
|
+
fs19.unlinkSync(path27.join(GROUPS_DIR, `${name}.sync.json`));
|
|
2628
2629
|
} catch {
|
|
2629
2630
|
}
|
|
2630
2631
|
}
|
|
@@ -2657,14 +2658,14 @@ function removeMember(groupName, groupPath) {
|
|
|
2657
2658
|
function saveSyncResult(result) {
|
|
2658
2659
|
fs19.mkdirSync(GROUPS_DIR, { recursive: true });
|
|
2659
2660
|
fs19.writeFileSync(
|
|
2660
|
-
|
|
2661
|
+
path27.join(GROUPS_DIR, `${result.groupName}.sync.json`),
|
|
2661
2662
|
JSON.stringify(result, null, 2) + "\n"
|
|
2662
2663
|
);
|
|
2663
2664
|
}
|
|
2664
2665
|
function loadSyncResult(groupName) {
|
|
2665
2666
|
try {
|
|
2666
2667
|
return JSON.parse(
|
|
2667
|
-
fs19.readFileSync(
|
|
2668
|
+
fs19.readFileSync(path27.join(GROUPS_DIR, `${groupName}.sync.json`), "utf-8")
|
|
2668
2669
|
);
|
|
2669
2670
|
} catch {
|
|
2670
2671
|
return null;
|
|
@@ -2673,7 +2674,7 @@ function loadSyncResult(groupName) {
|
|
|
2673
2674
|
var GROUPS_DIR;
|
|
2674
2675
|
var init_group_registry = __esm({
|
|
2675
2676
|
"src/multi-repo/group-registry.ts"() {
|
|
2676
|
-
GROUPS_DIR =
|
|
2677
|
+
GROUPS_DIR = path27.join(os12.homedir(), ".code-intel", "groups");
|
|
2677
2678
|
}
|
|
2678
2679
|
});
|
|
2679
2680
|
|
|
@@ -2926,9 +2927,9 @@ var init_orphan_files = __esm({
|
|
|
2926
2927
|
// src/health/health-score.ts
|
|
2927
2928
|
var health_score_exports = {};
|
|
2928
2929
|
__export(health_score_exports, {
|
|
2929
|
-
computeHealthReport: () =>
|
|
2930
|
+
computeHealthReport: () => computeHealthReport2
|
|
2930
2931
|
});
|
|
2931
|
-
function
|
|
2932
|
+
function computeHealthReport2(graph, godNodeConfig) {
|
|
2932
2933
|
const deadCode = detectDeadCode(graph);
|
|
2933
2934
|
const cycles = detectCircularDeps(graph);
|
|
2934
2935
|
const godNodes = detectGodNodes(graph, godNodeConfig);
|
|
@@ -2954,6 +2955,753 @@ var init_health_score = __esm({
|
|
|
2954
2955
|
}
|
|
2955
2956
|
});
|
|
2956
2957
|
|
|
2958
|
+
// src/query/gql-parser.ts
|
|
2959
|
+
var gql_parser_exports = {};
|
|
2960
|
+
__export(gql_parser_exports, {
|
|
2961
|
+
isGQLParseError: () => isGQLParseError,
|
|
2962
|
+
parseGQL: () => parseGQL
|
|
2963
|
+
});
|
|
2964
|
+
function isGQLParseError(v) {
|
|
2965
|
+
return v.type === "GQLParseError";
|
|
2966
|
+
}
|
|
2967
|
+
function tokenize(input) {
|
|
2968
|
+
const tokens = [];
|
|
2969
|
+
let i = 0;
|
|
2970
|
+
const len = input.length;
|
|
2971
|
+
while (i < len) {
|
|
2972
|
+
if (/\s/.test(input[i])) {
|
|
2973
|
+
i++;
|
|
2974
|
+
continue;
|
|
2975
|
+
}
|
|
2976
|
+
if (input[i] === "#") {
|
|
2977
|
+
while (i < len && input[i] !== "\n") i++;
|
|
2978
|
+
continue;
|
|
2979
|
+
}
|
|
2980
|
+
const pos = i;
|
|
2981
|
+
if (input[i] === '"' || input[i] === "'") {
|
|
2982
|
+
const quote = input[i];
|
|
2983
|
+
i++;
|
|
2984
|
+
let str = "";
|
|
2985
|
+
while (i < len && input[i] !== quote) {
|
|
2986
|
+
if (input[i] === "\\") {
|
|
2987
|
+
i++;
|
|
2988
|
+
if (i < len) {
|
|
2989
|
+
const esc = input[i];
|
|
2990
|
+
str += esc === "n" ? "\n" : esc === "t" ? " " : esc;
|
|
2991
|
+
i++;
|
|
2992
|
+
}
|
|
2993
|
+
} else {
|
|
2994
|
+
str += input[i++];
|
|
2995
|
+
}
|
|
2996
|
+
}
|
|
2997
|
+
if (i >= len) {
|
|
2998
|
+
return { type: "GQLParseError", message: `Unterminated string at position ${pos}`, pos };
|
|
2999
|
+
}
|
|
3000
|
+
i++;
|
|
3001
|
+
tokens.push({ kind: "STRING", value: str, pos });
|
|
3002
|
+
continue;
|
|
3003
|
+
}
|
|
3004
|
+
if (/[0-9]/.test(input[i])) {
|
|
3005
|
+
let num = "";
|
|
3006
|
+
while (i < len && /[0-9]/.test(input[i])) num += input[i++];
|
|
3007
|
+
tokens.push({ kind: "NUMBER", value: num, pos });
|
|
3008
|
+
continue;
|
|
3009
|
+
}
|
|
3010
|
+
if (input[i] === "[") {
|
|
3011
|
+
tokens.push({ kind: "LBRACKET", value: "[", pos });
|
|
3012
|
+
i++;
|
|
3013
|
+
continue;
|
|
3014
|
+
}
|
|
3015
|
+
if (input[i] === "]") {
|
|
3016
|
+
tokens.push({ kind: "RBRACKET", value: "]", pos });
|
|
3017
|
+
i++;
|
|
3018
|
+
continue;
|
|
3019
|
+
}
|
|
3020
|
+
if (input[i] === "*") {
|
|
3021
|
+
tokens.push({ kind: "STAR", value: "*", pos });
|
|
3022
|
+
i++;
|
|
3023
|
+
continue;
|
|
3024
|
+
}
|
|
3025
|
+
if (input[i] === "!" && input[i + 1] === "=") {
|
|
3026
|
+
tokens.push({ kind: "OPERATOR", value: "!=", pos });
|
|
3027
|
+
i += 2;
|
|
3028
|
+
continue;
|
|
3029
|
+
}
|
|
3030
|
+
if (input[i] === "=") {
|
|
3031
|
+
tokens.push({ kind: "OPERATOR", value: "=", pos });
|
|
3032
|
+
i++;
|
|
3033
|
+
continue;
|
|
3034
|
+
}
|
|
3035
|
+
if (/[a-zA-Z_]/.test(input[i])) {
|
|
3036
|
+
let ident = "";
|
|
3037
|
+
while (i < len && /[a-zA-Z0-9_]/.test(input[i])) ident += input[i++];
|
|
3038
|
+
const upper = ident.toUpperCase();
|
|
3039
|
+
if (upper === "CONTAINS" || upper === "STARTS_WITH" || upper === "IN") {
|
|
3040
|
+
tokens.push({ kind: "OPERATOR", value: upper, pos });
|
|
3041
|
+
} else if (KEYWORDS.has(upper)) {
|
|
3042
|
+
tokens.push({ kind: "KEYWORD", value: upper, pos });
|
|
3043
|
+
} else {
|
|
3044
|
+
tokens.push({ kind: "IDENT", value: ident, pos });
|
|
3045
|
+
}
|
|
3046
|
+
continue;
|
|
3047
|
+
}
|
|
3048
|
+
if (input[i] === ",") {
|
|
3049
|
+
i++;
|
|
3050
|
+
continue;
|
|
3051
|
+
}
|
|
3052
|
+
return {
|
|
3053
|
+
type: "GQLParseError",
|
|
3054
|
+
message: `Unexpected character '${input[i]}' at position ${i}`,
|
|
3055
|
+
pos: i
|
|
3056
|
+
};
|
|
3057
|
+
}
|
|
3058
|
+
tokens.push({ kind: "EOF", value: "", pos: len });
|
|
3059
|
+
return tokens;
|
|
3060
|
+
}
|
|
3061
|
+
function parseGQL(input) {
|
|
3062
|
+
const tokens = tokenize(input.trim());
|
|
3063
|
+
if (!Array.isArray(tokens)) return tokens;
|
|
3064
|
+
const parser = new Parser2(tokens);
|
|
3065
|
+
return parser.parse();
|
|
3066
|
+
}
|
|
3067
|
+
var KEYWORDS, Parser2;
|
|
3068
|
+
var init_gql_parser = __esm({
|
|
3069
|
+
"src/query/gql-parser.ts"() {
|
|
3070
|
+
KEYWORDS = /* @__PURE__ */ new Set([
|
|
3071
|
+
"FIND",
|
|
3072
|
+
"TRAVERSE",
|
|
3073
|
+
"PATH",
|
|
3074
|
+
"COUNT",
|
|
3075
|
+
"WHERE",
|
|
3076
|
+
"FROM",
|
|
3077
|
+
"TO",
|
|
3078
|
+
"IN",
|
|
3079
|
+
"BY",
|
|
3080
|
+
"AND",
|
|
3081
|
+
"NOT",
|
|
3082
|
+
"LIMIT",
|
|
3083
|
+
"OFFSET",
|
|
3084
|
+
"DEPTH",
|
|
3085
|
+
"GROUP",
|
|
3086
|
+
"CONTAINS",
|
|
3087
|
+
"STARTS_WITH",
|
|
3088
|
+
"CALLS",
|
|
3089
|
+
"IMPORTS",
|
|
3090
|
+
"EXTENDS",
|
|
3091
|
+
"IMPLEMENTS",
|
|
3092
|
+
"HAS_MEMBER",
|
|
3093
|
+
"ACCESSES",
|
|
3094
|
+
"OVERRIDES",
|
|
3095
|
+
"BELONGS_TO",
|
|
3096
|
+
"STEP_OF",
|
|
3097
|
+
"HANDLES",
|
|
3098
|
+
"CONTAINS_EDGE",
|
|
3099
|
+
"OUTGOING",
|
|
3100
|
+
"INCOMING",
|
|
3101
|
+
"BOTH"
|
|
3102
|
+
]);
|
|
3103
|
+
Parser2 = class {
|
|
3104
|
+
tokens;
|
|
3105
|
+
pos = 0;
|
|
3106
|
+
constructor(tokens) {
|
|
3107
|
+
this.tokens = tokens;
|
|
3108
|
+
}
|
|
3109
|
+
peek() {
|
|
3110
|
+
return this.tokens[this.pos];
|
|
3111
|
+
}
|
|
3112
|
+
consume() {
|
|
3113
|
+
return this.tokens[this.pos++];
|
|
3114
|
+
}
|
|
3115
|
+
expect(kind, value) {
|
|
3116
|
+
const tok = this.peek();
|
|
3117
|
+
if (tok.kind !== kind) {
|
|
3118
|
+
return {
|
|
3119
|
+
type: "GQLParseError",
|
|
3120
|
+
message: `Expected ${value ?? kind} but got '${tok.value}' at position ${tok.pos}`,
|
|
3121
|
+
pos: tok.pos,
|
|
3122
|
+
expected: value ?? kind,
|
|
3123
|
+
got: tok.value
|
|
3124
|
+
};
|
|
3125
|
+
}
|
|
3126
|
+
if (value !== void 0 && tok.value !== value) {
|
|
3127
|
+
return {
|
|
3128
|
+
type: "GQLParseError",
|
|
3129
|
+
message: `Expected '${value}' but got '${tok.value}' at position ${tok.pos}`,
|
|
3130
|
+
pos: tok.pos,
|
|
3131
|
+
expected: value,
|
|
3132
|
+
got: tok.value
|
|
3133
|
+
};
|
|
3134
|
+
}
|
|
3135
|
+
return this.consume();
|
|
3136
|
+
}
|
|
3137
|
+
matchKeyword(...values) {
|
|
3138
|
+
const tok = this.peek();
|
|
3139
|
+
return tok.kind === "KEYWORD" && values.includes(tok.value);
|
|
3140
|
+
}
|
|
3141
|
+
optionalKeyword(...values) {
|
|
3142
|
+
if (this.matchKeyword(...values)) {
|
|
3143
|
+
return this.consume();
|
|
3144
|
+
}
|
|
3145
|
+
return null;
|
|
3146
|
+
}
|
|
3147
|
+
/** Parse the node kind filter (IDENT, KEYWORD that's a kind, or STAR) */
|
|
3148
|
+
parseNodeKind() {
|
|
3149
|
+
const tok = this.peek();
|
|
3150
|
+
if (tok.kind === "STAR") {
|
|
3151
|
+
this.consume();
|
|
3152
|
+
return "*";
|
|
3153
|
+
}
|
|
3154
|
+
if (tok.kind === "IDENT" || tok.kind === "KEYWORD") {
|
|
3155
|
+
this.consume();
|
|
3156
|
+
return tok.value.toLowerCase();
|
|
3157
|
+
}
|
|
3158
|
+
return {
|
|
3159
|
+
type: "GQLParseError",
|
|
3160
|
+
message: `Expected node kind or '*' at position ${tok.pos}`,
|
|
3161
|
+
pos: tok.pos
|
|
3162
|
+
};
|
|
3163
|
+
}
|
|
3164
|
+
/** Parse a string value (STRING or IDENT) */
|
|
3165
|
+
parseStringValue() {
|
|
3166
|
+
const tok = this.peek();
|
|
3167
|
+
if (tok.kind === "STRING") {
|
|
3168
|
+
this.consume();
|
|
3169
|
+
return tok.value;
|
|
3170
|
+
}
|
|
3171
|
+
if (tok.kind === "IDENT" || tok.kind === "KEYWORD") {
|
|
3172
|
+
this.consume();
|
|
3173
|
+
return tok.value;
|
|
3174
|
+
}
|
|
3175
|
+
return {
|
|
3176
|
+
type: "GQLParseError",
|
|
3177
|
+
message: `Expected string value at position ${tok.pos}`,
|
|
3178
|
+
pos: tok.pos
|
|
3179
|
+
};
|
|
3180
|
+
}
|
|
3181
|
+
/** Parse an IN list: [ value, value, ... ] */
|
|
3182
|
+
parseInList() {
|
|
3183
|
+
const lb = this.expect("LBRACKET");
|
|
3184
|
+
if (isGQLParseError(lb)) return lb;
|
|
3185
|
+
const values = [];
|
|
3186
|
+
while (!this.matchKeyword() && this.peek().kind !== "RBRACKET" && this.peek().kind !== "EOF") {
|
|
3187
|
+
const v = this.parseStringValue();
|
|
3188
|
+
if (typeof v !== "string") return v;
|
|
3189
|
+
values.push(v);
|
|
3190
|
+
}
|
|
3191
|
+
const rb = this.expect("RBRACKET");
|
|
3192
|
+
if (isGQLParseError(rb)) return rb;
|
|
3193
|
+
return values;
|
|
3194
|
+
}
|
|
3195
|
+
/** Parse a single WHERE expression */
|
|
3196
|
+
parseWhereExpr() {
|
|
3197
|
+
const propTok = this.peek();
|
|
3198
|
+
if (propTok.kind !== "IDENT" && propTok.kind !== "KEYWORD") {
|
|
3199
|
+
return {
|
|
3200
|
+
type: "GQLParseError",
|
|
3201
|
+
message: `Expected property name at position ${propTok.pos}`,
|
|
3202
|
+
pos: propTok.pos
|
|
3203
|
+
};
|
|
3204
|
+
}
|
|
3205
|
+
this.consume();
|
|
3206
|
+
const property = propTok.value.toLowerCase();
|
|
3207
|
+
const opTok = this.peek();
|
|
3208
|
+
if (opTok.kind !== "OPERATOR") {
|
|
3209
|
+
return {
|
|
3210
|
+
type: "GQLParseError",
|
|
3211
|
+
message: `Expected operator (=, !=, CONTAINS, STARTS_WITH, IN) at position ${opTok.pos}`,
|
|
3212
|
+
pos: opTok.pos,
|
|
3213
|
+
expected: "operator",
|
|
3214
|
+
got: opTok.value
|
|
3215
|
+
};
|
|
3216
|
+
}
|
|
3217
|
+
this.consume();
|
|
3218
|
+
const operator = opTok.value;
|
|
3219
|
+
if (operator === "IN") {
|
|
3220
|
+
const list = this.parseInList();
|
|
3221
|
+
if (!Array.isArray(list)) return list;
|
|
3222
|
+
return { property, operator, value: list };
|
|
3223
|
+
}
|
|
3224
|
+
const val = this.parseStringValue();
|
|
3225
|
+
if (typeof val !== "string") return val;
|
|
3226
|
+
return { property, operator, value: val };
|
|
3227
|
+
}
|
|
3228
|
+
/** Parse WHERE clause: WHERE expr (AND expr)* */
|
|
3229
|
+
parseWhereClause() {
|
|
3230
|
+
const kw = this.expect("KEYWORD", "WHERE");
|
|
3231
|
+
if (isGQLParseError(kw)) return kw;
|
|
3232
|
+
const exprs = [];
|
|
3233
|
+
const first = this.parseWhereExpr();
|
|
3234
|
+
if ("type" in first && first.type === "GQLParseError") return first;
|
|
3235
|
+
exprs.push(first);
|
|
3236
|
+
while (this.matchKeyword("AND")) {
|
|
3237
|
+
this.consume();
|
|
3238
|
+
const expr = this.parseWhereExpr();
|
|
3239
|
+
if ("type" in expr && expr.type === "GQLParseError") return expr;
|
|
3240
|
+
exprs.push(expr);
|
|
3241
|
+
}
|
|
3242
|
+
return { exprs };
|
|
3243
|
+
}
|
|
3244
|
+
/** Parse FIND statement */
|
|
3245
|
+
parseFindStatement() {
|
|
3246
|
+
this.consume();
|
|
3247
|
+
const kind = this.parseNodeKind();
|
|
3248
|
+
if (typeof kind !== "string") return kind;
|
|
3249
|
+
let where;
|
|
3250
|
+
if (this.matchKeyword("WHERE")) {
|
|
3251
|
+
const w = this.parseWhereClause();
|
|
3252
|
+
if ("type" in w && w.type === "GQLParseError") return w;
|
|
3253
|
+
where = w;
|
|
3254
|
+
}
|
|
3255
|
+
let limit;
|
|
3256
|
+
let offset;
|
|
3257
|
+
while (this.matchKeyword("LIMIT", "OFFSET")) {
|
|
3258
|
+
const kw = this.consume();
|
|
3259
|
+
const numTok = this.peek();
|
|
3260
|
+
if (numTok.kind !== "NUMBER") {
|
|
3261
|
+
return {
|
|
3262
|
+
type: "GQLParseError",
|
|
3263
|
+
message: `Expected number after ${kw.value} at position ${numTok.pos}`,
|
|
3264
|
+
pos: numTok.pos
|
|
3265
|
+
};
|
|
3266
|
+
}
|
|
3267
|
+
this.consume();
|
|
3268
|
+
const n = parseInt(numTok.value, 10);
|
|
3269
|
+
if (kw.value === "LIMIT") limit = n;
|
|
3270
|
+
else offset = n;
|
|
3271
|
+
}
|
|
3272
|
+
return { type: "FIND", target: kind, where, limit, offset };
|
|
3273
|
+
}
|
|
3274
|
+
/** Parse TRAVERSE statement */
|
|
3275
|
+
parseTraverseStatement() {
|
|
3276
|
+
this.consume();
|
|
3277
|
+
const edgeTok = this.peek();
|
|
3278
|
+
if (edgeTok.kind !== "KEYWORD" && edgeTok.kind !== "IDENT") {
|
|
3279
|
+
return {
|
|
3280
|
+
type: "GQLParseError",
|
|
3281
|
+
message: `Expected edge kind after TRAVERSE at position ${edgeTok.pos}`,
|
|
3282
|
+
pos: edgeTok.pos
|
|
3283
|
+
};
|
|
3284
|
+
}
|
|
3285
|
+
this.consume();
|
|
3286
|
+
const edgeKind = edgeTok.value.toLowerCase();
|
|
3287
|
+
const fromKw = this.expect("KEYWORD", "FROM");
|
|
3288
|
+
if (isGQLParseError(fromKw)) return fromKw;
|
|
3289
|
+
const fromVal = this.parseStringValue();
|
|
3290
|
+
if (typeof fromVal !== "string") return fromVal;
|
|
3291
|
+
let depth;
|
|
3292
|
+
let direction;
|
|
3293
|
+
if (this.matchKeyword("DEPTH")) {
|
|
3294
|
+
this.consume();
|
|
3295
|
+
const numTok = this.peek();
|
|
3296
|
+
if (numTok.kind !== "NUMBER") {
|
|
3297
|
+
return {
|
|
3298
|
+
type: "GQLParseError",
|
|
3299
|
+
message: `Expected number after DEPTH at position ${numTok.pos}`,
|
|
3300
|
+
pos: numTok.pos
|
|
3301
|
+
};
|
|
3302
|
+
}
|
|
3303
|
+
this.consume();
|
|
3304
|
+
depth = parseInt(numTok.value, 10);
|
|
3305
|
+
}
|
|
3306
|
+
if (this.matchKeyword("OUTGOING", "INCOMING", "BOTH")) {
|
|
3307
|
+
direction = this.consume().value;
|
|
3308
|
+
}
|
|
3309
|
+
return { type: "TRAVERSE", edgeKind, from: fromVal, depth, direction };
|
|
3310
|
+
}
|
|
3311
|
+
/** Parse PATH statement */
|
|
3312
|
+
parsePathStatement() {
|
|
3313
|
+
this.consume();
|
|
3314
|
+
const fromKw = this.expect("KEYWORD", "FROM");
|
|
3315
|
+
if (isGQLParseError(fromKw)) return fromKw;
|
|
3316
|
+
const fromVal = this.parseStringValue();
|
|
3317
|
+
if (typeof fromVal !== "string") return fromVal;
|
|
3318
|
+
const toKw = this.expect("KEYWORD", "TO");
|
|
3319
|
+
if (isGQLParseError(toKw)) return toKw;
|
|
3320
|
+
const toVal = this.parseStringValue();
|
|
3321
|
+
if (typeof toVal !== "string") return toVal;
|
|
3322
|
+
return { type: "PATH", from: fromVal, to: toVal };
|
|
3323
|
+
}
|
|
3324
|
+
/** Parse COUNT statement */
|
|
3325
|
+
parseCountStatement() {
|
|
3326
|
+
this.consume();
|
|
3327
|
+
const kind = this.parseNodeKind();
|
|
3328
|
+
if (typeof kind !== "string") return kind;
|
|
3329
|
+
let where;
|
|
3330
|
+
if (this.matchKeyword("WHERE")) {
|
|
3331
|
+
const w = this.parseWhereClause();
|
|
3332
|
+
if ("type" in w && w.type === "GQLParseError") return w;
|
|
3333
|
+
where = w;
|
|
3334
|
+
}
|
|
3335
|
+
let groupBy;
|
|
3336
|
+
if (this.matchKeyword("GROUP")) {
|
|
3337
|
+
this.consume();
|
|
3338
|
+
const byKw = this.expect("KEYWORD", "BY");
|
|
3339
|
+
if (isGQLParseError(byKw)) return byKw;
|
|
3340
|
+
const propTok = this.peek();
|
|
3341
|
+
if (propTok.kind !== "IDENT" && propTok.kind !== "KEYWORD") {
|
|
3342
|
+
return {
|
|
3343
|
+
type: "GQLParseError",
|
|
3344
|
+
message: `Expected property name after GROUP BY at position ${propTok.pos}`,
|
|
3345
|
+
pos: propTok.pos
|
|
3346
|
+
};
|
|
3347
|
+
}
|
|
3348
|
+
this.consume();
|
|
3349
|
+
groupBy = propTok.value.toLowerCase();
|
|
3350
|
+
}
|
|
3351
|
+
return { type: "COUNT", target: kind, where, groupBy };
|
|
3352
|
+
}
|
|
3353
|
+
parse() {
|
|
3354
|
+
const tok = this.peek();
|
|
3355
|
+
if (tok.kind !== "KEYWORD") {
|
|
3356
|
+
return {
|
|
3357
|
+
type: "GQLParseError",
|
|
3358
|
+
message: `Expected FIND, TRAVERSE, PATH, or COUNT at position ${tok.pos}`,
|
|
3359
|
+
pos: tok.pos,
|
|
3360
|
+
expected: "FIND | TRAVERSE | PATH | COUNT",
|
|
3361
|
+
got: tok.value
|
|
3362
|
+
};
|
|
3363
|
+
}
|
|
3364
|
+
let result;
|
|
3365
|
+
switch (tok.value) {
|
|
3366
|
+
case "FIND":
|
|
3367
|
+
result = this.parseFindStatement();
|
|
3368
|
+
break;
|
|
3369
|
+
case "TRAVERSE":
|
|
3370
|
+
result = this.parseTraverseStatement();
|
|
3371
|
+
break;
|
|
3372
|
+
case "PATH":
|
|
3373
|
+
result = this.parsePathStatement();
|
|
3374
|
+
break;
|
|
3375
|
+
case "COUNT":
|
|
3376
|
+
result = this.parseCountStatement();
|
|
3377
|
+
break;
|
|
3378
|
+
default:
|
|
3379
|
+
return {
|
|
3380
|
+
type: "GQLParseError",
|
|
3381
|
+
message: `Unknown statement type '${tok.value}' at position ${tok.pos}`,
|
|
3382
|
+
pos: tok.pos,
|
|
3383
|
+
expected: "FIND | TRAVERSE | PATH | COUNT",
|
|
3384
|
+
got: tok.value
|
|
3385
|
+
};
|
|
3386
|
+
}
|
|
3387
|
+
if (isGQLParseError(result)) return result;
|
|
3388
|
+
const remaining = this.peek();
|
|
3389
|
+
if (remaining.kind !== "EOF") {
|
|
3390
|
+
return {
|
|
3391
|
+
type: "GQLParseError",
|
|
3392
|
+
message: `Unexpected token '${remaining.value}' at position ${remaining.pos}`,
|
|
3393
|
+
pos: remaining.pos,
|
|
3394
|
+
got: remaining.value
|
|
3395
|
+
};
|
|
3396
|
+
}
|
|
3397
|
+
return result;
|
|
3398
|
+
}
|
|
3399
|
+
};
|
|
3400
|
+
}
|
|
3401
|
+
});
|
|
3402
|
+
|
|
3403
|
+
// src/query/gql-executor.ts
|
|
3404
|
+
var gql_executor_exports = {};
|
|
3405
|
+
__export(gql_executor_exports, {
|
|
3406
|
+
executeGQL: () => executeGQL
|
|
3407
|
+
});
|
|
3408
|
+
function getNodeProperty(node, property) {
|
|
3409
|
+
switch (property) {
|
|
3410
|
+
case "name":
|
|
3411
|
+
return node.name;
|
|
3412
|
+
case "kind":
|
|
3413
|
+
return node.kind;
|
|
3414
|
+
case "filepath":
|
|
3415
|
+
case "filePath":
|
|
3416
|
+
return node.filePath;
|
|
3417
|
+
case "exported":
|
|
3418
|
+
return node.exported;
|
|
3419
|
+
case "language":
|
|
3420
|
+
return node.metadata?.language ?? void 0;
|
|
3421
|
+
case "cluster":
|
|
3422
|
+
return node.metadata?.cluster ?? void 0;
|
|
3423
|
+
default:
|
|
3424
|
+
return node.metadata?.[property] ?? void 0;
|
|
3425
|
+
}
|
|
3426
|
+
}
|
|
3427
|
+
function evaluateExpr(node, expr) {
|
|
3428
|
+
const val = getNodeProperty(node, expr.property);
|
|
3429
|
+
if (val === void 0) return false;
|
|
3430
|
+
const strVal = String(val).toLowerCase();
|
|
3431
|
+
switch (expr.operator) {
|
|
3432
|
+
case "=":
|
|
3433
|
+
if (typeof expr.value === "string") {
|
|
3434
|
+
return strVal === expr.value.toLowerCase();
|
|
3435
|
+
}
|
|
3436
|
+
return false;
|
|
3437
|
+
case "!=":
|
|
3438
|
+
if (typeof expr.value === "string") {
|
|
3439
|
+
return strVal !== expr.value.toLowerCase();
|
|
3440
|
+
}
|
|
3441
|
+
return true;
|
|
3442
|
+
case "CONTAINS":
|
|
3443
|
+
if (typeof expr.value === "string") {
|
|
3444
|
+
return strVal.includes(expr.value.toLowerCase());
|
|
3445
|
+
}
|
|
3446
|
+
return false;
|
|
3447
|
+
case "STARTS_WITH":
|
|
3448
|
+
if (typeof expr.value === "string") {
|
|
3449
|
+
return strVal.startsWith(expr.value.toLowerCase());
|
|
3450
|
+
}
|
|
3451
|
+
return false;
|
|
3452
|
+
case "IN":
|
|
3453
|
+
if (Array.isArray(expr.value)) {
|
|
3454
|
+
return expr.value.some((v) => strVal === v.toLowerCase());
|
|
3455
|
+
}
|
|
3456
|
+
return false;
|
|
3457
|
+
default:
|
|
3458
|
+
return false;
|
|
3459
|
+
}
|
|
3460
|
+
}
|
|
3461
|
+
function evaluateWhere(node, where) {
|
|
3462
|
+
return where.exprs.every((expr) => evaluateExpr(node, expr));
|
|
3463
|
+
}
|
|
3464
|
+
function executeFIND(stmt, graph) {
|
|
3465
|
+
const start = Date.now();
|
|
3466
|
+
const limit = stmt.limit ?? 1e3;
|
|
3467
|
+
const offset = stmt.offset ?? 0;
|
|
3468
|
+
let totalCount = 0;
|
|
3469
|
+
let truncated = false;
|
|
3470
|
+
const allMatching = [];
|
|
3471
|
+
const deadline = start + EXECUTION_TIMEOUT_MS;
|
|
3472
|
+
for (const node of graph.allNodes()) {
|
|
3473
|
+
if (Date.now() > deadline) {
|
|
3474
|
+
truncated = true;
|
|
3475
|
+
break;
|
|
3476
|
+
}
|
|
3477
|
+
if (stmt.target !== "*" && node.kind !== stmt.target) continue;
|
|
3478
|
+
if (stmt.where && !evaluateWhere(node, stmt.where)) continue;
|
|
3479
|
+
allMatching.push(node);
|
|
3480
|
+
}
|
|
3481
|
+
totalCount = allMatching.length;
|
|
3482
|
+
const paginated = allMatching.slice(offset, offset + limit);
|
|
3483
|
+
return {
|
|
3484
|
+
nodes: paginated,
|
|
3485
|
+
executionTimeMs: Date.now() - start,
|
|
3486
|
+
truncated,
|
|
3487
|
+
totalCount
|
|
3488
|
+
};
|
|
3489
|
+
}
|
|
3490
|
+
function executeTRAVERSE(stmt, graph) {
|
|
3491
|
+
const start = Date.now();
|
|
3492
|
+
const maxDepth = stmt.depth ?? 5;
|
|
3493
|
+
const edgeKind = stmt.edgeKind;
|
|
3494
|
+
const direction = stmt.direction ?? "OUTGOING";
|
|
3495
|
+
const deadline = start + EXECUTION_TIMEOUT_MS;
|
|
3496
|
+
let startNode;
|
|
3497
|
+
for (const node of graph.allNodes()) {
|
|
3498
|
+
if (node.name === stmt.from) {
|
|
3499
|
+
startNode = node;
|
|
3500
|
+
break;
|
|
3501
|
+
}
|
|
3502
|
+
}
|
|
3503
|
+
if (!startNode) {
|
|
3504
|
+
return {
|
|
3505
|
+
nodes: [],
|
|
3506
|
+
edges: [],
|
|
3507
|
+
executionTimeMs: Date.now() - start,
|
|
3508
|
+
truncated: false,
|
|
3509
|
+
totalCount: 0
|
|
3510
|
+
};
|
|
3511
|
+
}
|
|
3512
|
+
const visitedNodes = /* @__PURE__ */ new Set();
|
|
3513
|
+
const visitedEdges = /* @__PURE__ */ new Set();
|
|
3514
|
+
const resultNodes = [];
|
|
3515
|
+
const resultEdges = [];
|
|
3516
|
+
const queue = [{ id: startNode.id, depth: 0 }];
|
|
3517
|
+
visitedNodes.add(startNode.id);
|
|
3518
|
+
resultNodes.push(startNode);
|
|
3519
|
+
let truncated = false;
|
|
3520
|
+
while (queue.length > 0) {
|
|
3521
|
+
if (Date.now() > deadline) {
|
|
3522
|
+
truncated = true;
|
|
3523
|
+
break;
|
|
3524
|
+
}
|
|
3525
|
+
const { id, depth } = queue.shift();
|
|
3526
|
+
if (depth >= maxDepth) continue;
|
|
3527
|
+
const nextEdges = [];
|
|
3528
|
+
if (direction === "OUTGOING" || direction === "BOTH") {
|
|
3529
|
+
for (const edge of graph.findEdgesFrom(id)) {
|
|
3530
|
+
if (!edgeKind || edge.kind === edgeKind) nextEdges.push(edge);
|
|
3531
|
+
}
|
|
3532
|
+
}
|
|
3533
|
+
if (direction === "INCOMING" || direction === "BOTH") {
|
|
3534
|
+
for (const edge of graph.findEdgesTo(id)) {
|
|
3535
|
+
if (!edgeKind || edge.kind === edgeKind) nextEdges.push(edge);
|
|
3536
|
+
}
|
|
3537
|
+
}
|
|
3538
|
+
for (const edge of nextEdges) {
|
|
3539
|
+
if (!visitedEdges.has(edge.id)) {
|
|
3540
|
+
visitedEdges.add(edge.id);
|
|
3541
|
+
resultEdges.push(edge);
|
|
3542
|
+
}
|
|
3543
|
+
const neighborId = direction === "INCOMING" ? edge.source : edge.target;
|
|
3544
|
+
const effectiveNeighborId = direction === "BOTH" ? edge.source === id ? edge.target : edge.source : neighborId;
|
|
3545
|
+
if (!visitedNodes.has(effectiveNeighborId)) {
|
|
3546
|
+
visitedNodes.add(effectiveNeighborId);
|
|
3547
|
+
const neighborNode = graph.getNode(effectiveNeighborId);
|
|
3548
|
+
if (neighborNode) {
|
|
3549
|
+
resultNodes.push(neighborNode);
|
|
3550
|
+
queue.push({ id: effectiveNeighborId, depth: depth + 1 });
|
|
3551
|
+
}
|
|
3552
|
+
}
|
|
3553
|
+
}
|
|
3554
|
+
}
|
|
3555
|
+
return {
|
|
3556
|
+
nodes: resultNodes,
|
|
3557
|
+
edges: resultEdges,
|
|
3558
|
+
executionTimeMs: Date.now() - start,
|
|
3559
|
+
truncated,
|
|
3560
|
+
totalCount: resultNodes.length
|
|
3561
|
+
};
|
|
3562
|
+
}
|
|
3563
|
+
function executePATH(stmt, graph) {
|
|
3564
|
+
const start = Date.now();
|
|
3565
|
+
const deadline = start + EXECUTION_TIMEOUT_MS;
|
|
3566
|
+
let startNode;
|
|
3567
|
+
let endNode;
|
|
3568
|
+
for (const node of graph.allNodes()) {
|
|
3569
|
+
if (node.name === stmt.from) startNode = node;
|
|
3570
|
+
if (node.name === stmt.to) endNode = node;
|
|
3571
|
+
if (startNode && endNode) break;
|
|
3572
|
+
}
|
|
3573
|
+
if (!startNode || !endNode) {
|
|
3574
|
+
return {
|
|
3575
|
+
path: null,
|
|
3576
|
+
nodes: [],
|
|
3577
|
+
executionTimeMs: Date.now() - start,
|
|
3578
|
+
truncated: false,
|
|
3579
|
+
totalCount: 0
|
|
3580
|
+
};
|
|
3581
|
+
}
|
|
3582
|
+
const visited = /* @__PURE__ */ new Set();
|
|
3583
|
+
const parent = /* @__PURE__ */ new Map();
|
|
3584
|
+
const queue = [startNode.id];
|
|
3585
|
+
visited.add(startNode.id);
|
|
3586
|
+
let found = false;
|
|
3587
|
+
let truncated = false;
|
|
3588
|
+
outer: while (queue.length > 0) {
|
|
3589
|
+
if (Date.now() > deadline) {
|
|
3590
|
+
truncated = true;
|
|
3591
|
+
break;
|
|
3592
|
+
}
|
|
3593
|
+
const current2 = queue.shift();
|
|
3594
|
+
for (const edge of graph.findEdgesFrom(current2)) {
|
|
3595
|
+
const next = edge.target;
|
|
3596
|
+
if (!visited.has(next)) {
|
|
3597
|
+
visited.add(next);
|
|
3598
|
+
parent.set(next, { nodeId: current2, edgeId: edge.id });
|
|
3599
|
+
if (next === endNode.id) {
|
|
3600
|
+
found = true;
|
|
3601
|
+
break outer;
|
|
3602
|
+
}
|
|
3603
|
+
queue.push(next);
|
|
3604
|
+
}
|
|
3605
|
+
}
|
|
3606
|
+
for (const edge of graph.findEdgesTo(current2)) {
|
|
3607
|
+
const next = edge.source;
|
|
3608
|
+
if (!visited.has(next)) {
|
|
3609
|
+
visited.add(next);
|
|
3610
|
+
parent.set(next, { nodeId: current2, edgeId: edge.id });
|
|
3611
|
+
if (next === endNode.id) {
|
|
3612
|
+
found = true;
|
|
3613
|
+
break outer;
|
|
3614
|
+
}
|
|
3615
|
+
queue.push(next);
|
|
3616
|
+
}
|
|
3617
|
+
}
|
|
3618
|
+
}
|
|
3619
|
+
if (!found) {
|
|
3620
|
+
return {
|
|
3621
|
+
path: null,
|
|
3622
|
+
nodes: [],
|
|
3623
|
+
executionTimeMs: Date.now() - start,
|
|
3624
|
+
truncated,
|
|
3625
|
+
totalCount: 0
|
|
3626
|
+
};
|
|
3627
|
+
}
|
|
3628
|
+
const pathNodeIds = [];
|
|
3629
|
+
const pathEdgeIds = [];
|
|
3630
|
+
let current = endNode.id;
|
|
3631
|
+
while (current !== startNode.id) {
|
|
3632
|
+
pathNodeIds.unshift(current);
|
|
3633
|
+
const p = parent.get(current);
|
|
3634
|
+
pathEdgeIds.unshift(p.edgeId);
|
|
3635
|
+
current = p.nodeId;
|
|
3636
|
+
}
|
|
3637
|
+
pathNodeIds.unshift(startNode.id);
|
|
3638
|
+
const pathNodes = pathNodeIds.map((id) => graph.getNode(id)).filter(Boolean);
|
|
3639
|
+
const pathEdges = pathEdgeIds.map((id) => graph.getEdge(id)).filter(Boolean);
|
|
3640
|
+
return {
|
|
3641
|
+
path: pathNodes,
|
|
3642
|
+
nodes: pathNodes,
|
|
3643
|
+
edges: pathEdges,
|
|
3644
|
+
executionTimeMs: Date.now() - start,
|
|
3645
|
+
truncated,
|
|
3646
|
+
totalCount: pathNodes.length
|
|
3647
|
+
};
|
|
3648
|
+
}
|
|
3649
|
+
function executeCOUNT(stmt, graph) {
|
|
3650
|
+
const start = Date.now();
|
|
3651
|
+
const deadline = start + EXECUTION_TIMEOUT_MS;
|
|
3652
|
+
let truncated = false;
|
|
3653
|
+
const groups = /* @__PURE__ */ new Map();
|
|
3654
|
+
let total = 0;
|
|
3655
|
+
for (const node of graph.allNodes()) {
|
|
3656
|
+
if (Date.now() > deadline) {
|
|
3657
|
+
truncated = true;
|
|
3658
|
+
break;
|
|
3659
|
+
}
|
|
3660
|
+
if (stmt.target !== "*" && node.kind !== stmt.target) continue;
|
|
3661
|
+
if (stmt.where && !evaluateWhere(node, stmt.where)) continue;
|
|
3662
|
+
total++;
|
|
3663
|
+
if (stmt.groupBy) {
|
|
3664
|
+
const key = String(getNodeProperty(node, stmt.groupBy) ?? "(none)");
|
|
3665
|
+
groups.set(key, (groups.get(key) ?? 0) + 1);
|
|
3666
|
+
} else {
|
|
3667
|
+
groups.set("total", (groups.get("total") ?? 0) + 1);
|
|
3668
|
+
}
|
|
3669
|
+
}
|
|
3670
|
+
const groupList = [...groups.entries()].map(([key, count]) => ({ key, count }));
|
|
3671
|
+
groupList.sort((a, b) => b.count - a.count);
|
|
3672
|
+
return {
|
|
3673
|
+
groups: groupList,
|
|
3674
|
+
executionTimeMs: Date.now() - start,
|
|
3675
|
+
truncated,
|
|
3676
|
+
totalCount: total
|
|
3677
|
+
};
|
|
3678
|
+
}
|
|
3679
|
+
function executeGQL(ast, graph) {
|
|
3680
|
+
switch (ast.type) {
|
|
3681
|
+
case "FIND":
|
|
3682
|
+
return executeFIND(ast, graph);
|
|
3683
|
+
case "TRAVERSE":
|
|
3684
|
+
return executeTRAVERSE(ast, graph);
|
|
3685
|
+
case "PATH":
|
|
3686
|
+
return executePATH(ast, graph);
|
|
3687
|
+
case "COUNT":
|
|
3688
|
+
return executeCOUNT(ast, graph);
|
|
3689
|
+
default:
|
|
3690
|
+
return {
|
|
3691
|
+
nodes: [],
|
|
3692
|
+
executionTimeMs: 0,
|
|
3693
|
+
truncated: false,
|
|
3694
|
+
totalCount: 0
|
|
3695
|
+
};
|
|
3696
|
+
}
|
|
3697
|
+
}
|
|
3698
|
+
var EXECUTION_TIMEOUT_MS;
|
|
3699
|
+
var init_gql_executor = __esm({
|
|
3700
|
+
"src/query/gql-executor.ts"() {
|
|
3701
|
+
EXECUTION_TIMEOUT_MS = 1e4;
|
|
3702
|
+
}
|
|
3703
|
+
});
|
|
3704
|
+
|
|
2957
3705
|
// src/errors/codes.ts
|
|
2958
3706
|
var ErrorCodes, AppError;
|
|
2959
3707
|
var init_codes = __esm({
|
|
@@ -3008,7 +3756,7 @@ function tightenDbFiles(dir) {
|
|
|
3008
3756
|
for (const name of fs19.readdirSync(dir)) {
|
|
3009
3757
|
if (name.endsWith(".db") || name.endsWith(".db-wal") || name.endsWith(".db-shm")) {
|
|
3010
3758
|
try {
|
|
3011
|
-
fs19.chmodSync(
|
|
3759
|
+
fs19.chmodSync(path27.join(dir, name), SECURE_FILE_MODE);
|
|
3012
3760
|
} catch {
|
|
3013
3761
|
}
|
|
3014
3762
|
}
|
|
@@ -3022,7 +3770,7 @@ var init_fs_secure = __esm({
|
|
|
3022
3770
|
}
|
|
3023
3771
|
});
|
|
3024
3772
|
function getUsersDBPath() {
|
|
3025
|
-
return process.env["CODE_INTEL_USERS_DB_PATH"] ??
|
|
3773
|
+
return process.env["CODE_INTEL_USERS_DB_PATH"] ?? path27.join(os12.homedir(), ".code-intel", "users.db");
|
|
3026
3774
|
}
|
|
3027
3775
|
function getOrCreateUsersDB() {
|
|
3028
3776
|
if (!_usersDB) {
|
|
@@ -3038,7 +3786,7 @@ var init_users_db = __esm({
|
|
|
3038
3786
|
UsersDB = class {
|
|
3039
3787
|
db;
|
|
3040
3788
|
constructor(dbPath) {
|
|
3041
|
-
const dir =
|
|
3789
|
+
const dir = path27.dirname(dbPath);
|
|
3042
3790
|
secureMkdir(dir);
|
|
3043
3791
|
this.db = new Database3(dbPath);
|
|
3044
3792
|
this.db.pragma("journal_mode = WAL");
|
|
@@ -3315,7 +4063,7 @@ function getScryptN() {
|
|
|
3315
4063
|
return Number.isInteger(v) && v >= 1024 ? v : 1 << 14;
|
|
3316
4064
|
}
|
|
3317
4065
|
function getSecretsPath() {
|
|
3318
|
-
return process.env["CODE_INTEL_SECRETS_PATH"] ??
|
|
4066
|
+
return process.env["CODE_INTEL_SECRETS_PATH"] ?? path27.join(os12.homedir(), ".code-intel", ".secrets");
|
|
3319
4067
|
}
|
|
3320
4068
|
function getMasterPassword() {
|
|
3321
4069
|
const fromEnv = process.env["CODE_INTEL_SECRET_KEY"];
|
|
@@ -3904,7 +4652,7 @@ init_shared();
|
|
|
3904
4652
|
init_shared();
|
|
3905
4653
|
init_typescript();
|
|
3906
4654
|
function resolveRelative(rawPath, fromFile, workspace) {
|
|
3907
|
-
const fromDir =
|
|
4655
|
+
const fromDir = path27.dirname(fromFile);
|
|
3908
4656
|
const cleaned = rawPath.replace(/['"]/g, "");
|
|
3909
4657
|
const extensions = [".ts", ".tsx", ".js", ".jsx", "/index.ts", "/index.js"];
|
|
3910
4658
|
const resolved = workspace.resolve(fromDir, cleaned);
|
|
@@ -3956,7 +4704,7 @@ var pythonModule = {
|
|
|
3956
4704
|
resolveImport(rawPath, fromFile, workspace) {
|
|
3957
4705
|
const cleaned = rawPath.replace(/['"]/g, "");
|
|
3958
4706
|
const parts = cleaned.split(".");
|
|
3959
|
-
const fromDir =
|
|
4707
|
+
const fromDir = path27.dirname(fromFile);
|
|
3960
4708
|
const relPath = parts.join("/");
|
|
3961
4709
|
for (const suffix of ["/__init__.py", ".py"]) {
|
|
3962
4710
|
const r = workspace.resolve(fromDir, relPath + suffix);
|
|
@@ -4035,7 +4783,7 @@ var cModule = {
|
|
|
4035
4783
|
inheritanceStrategy: "none",
|
|
4036
4784
|
resolveImport(rawPath, fromFile, workspace) {
|
|
4037
4785
|
const cleaned = rawPath.replace(/[<>"']/g, "");
|
|
4038
|
-
const fromDir =
|
|
4786
|
+
const fromDir = path27.dirname(fromFile);
|
|
4039
4787
|
return workspace.resolve(fromDir, cleaned);
|
|
4040
4788
|
},
|
|
4041
4789
|
isExported(_node) {
|
|
@@ -4058,7 +4806,7 @@ var cppModule = {
|
|
|
4058
4806
|
inheritanceStrategy: "depth-first",
|
|
4059
4807
|
resolveImport(rawPath, fromFile, workspace) {
|
|
4060
4808
|
const cleaned = rawPath.replace(/[<>"']/g, "");
|
|
4061
|
-
const fromDir =
|
|
4809
|
+
const fromDir = path27.dirname(fromFile);
|
|
4062
4810
|
return workspace.resolve(fromDir, cleaned);
|
|
4063
4811
|
},
|
|
4064
4812
|
isExported(_node) {
|
|
@@ -4220,7 +4968,7 @@ var dartModule = {
|
|
|
4220
4968
|
const pkg = cleaned.replace("package:", "");
|
|
4221
4969
|
return workspace.findByPackage(pkg);
|
|
4222
4970
|
}
|
|
4223
|
-
const fromDir =
|
|
4971
|
+
const fromDir = path27.dirname(fromFile);
|
|
4224
4972
|
return workspace.resolve(fromDir, cleaned);
|
|
4225
4973
|
},
|
|
4226
4974
|
isExported(node) {
|
|
@@ -4806,7 +5554,7 @@ var IGNORED_DIRS = /* @__PURE__ */ new Set([
|
|
|
4806
5554
|
]);
|
|
4807
5555
|
function loadIgnorePatterns(workspaceRoot) {
|
|
4808
5556
|
try {
|
|
4809
|
-
const raw = fs19.readFileSync(
|
|
5557
|
+
const raw = fs19.readFileSync(path27.join(workspaceRoot, ".codeintelignore"), "utf-8");
|
|
4810
5558
|
const extras = /* @__PURE__ */ new Set();
|
|
4811
5559
|
for (const line of raw.split("\n")) {
|
|
4812
5560
|
const trimmed = line.trim();
|
|
@@ -4839,13 +5587,13 @@ var scanPhase = {
|
|
|
4839
5587
|
if (entry.name.startsWith(".")) continue;
|
|
4840
5588
|
if (IGNORED_DIRS.has(entry.name)) continue;
|
|
4841
5589
|
if (extraIgnore.has(entry.name)) continue;
|
|
4842
|
-
walk(
|
|
5590
|
+
walk(path27.join(dir, entry.name));
|
|
4843
5591
|
} else if (entry.isFile()) {
|
|
4844
5592
|
const name = entry.name;
|
|
4845
5593
|
if (IGNORED_FILE_SUFFIXES.some((s) => name.endsWith(s))) continue;
|
|
4846
|
-
const ext =
|
|
5594
|
+
const ext = path27.extname(name);
|
|
4847
5595
|
if (!extensions.has(ext)) continue;
|
|
4848
|
-
const fullPath =
|
|
5596
|
+
const fullPath = path27.join(dir, name);
|
|
4849
5597
|
try {
|
|
4850
5598
|
const stat = fs19.statSync(fullPath);
|
|
4851
5599
|
if (stat.size > MAX_FILE_SIZE_BYTES) continue;
|
|
@@ -4874,20 +5622,20 @@ var structurePhase = {
|
|
|
4874
5622
|
const dirs = /* @__PURE__ */ new Set();
|
|
4875
5623
|
let structDone = 0;
|
|
4876
5624
|
for (const filePath of context2.filePaths) {
|
|
4877
|
-
const relativePath =
|
|
5625
|
+
const relativePath = path27.relative(context2.workspaceRoot, filePath);
|
|
4878
5626
|
const lang = detectLanguage(filePath);
|
|
4879
5627
|
context2.graph.addNode({
|
|
4880
5628
|
id: generateNodeId("file", relativePath, relativePath),
|
|
4881
5629
|
kind: "file",
|
|
4882
|
-
name:
|
|
5630
|
+
name: path27.basename(filePath),
|
|
4883
5631
|
filePath: relativePath,
|
|
4884
5632
|
metadata: lang ? { language: lang } : void 0
|
|
4885
5633
|
});
|
|
4886
|
-
let dir =
|
|
5634
|
+
let dir = path27.dirname(relativePath);
|
|
4887
5635
|
while (dir && dir !== "." && dir !== "") {
|
|
4888
5636
|
if (dirs.has(dir)) break;
|
|
4889
5637
|
dirs.add(dir);
|
|
4890
|
-
dir =
|
|
5638
|
+
dir = path27.dirname(dir);
|
|
4891
5639
|
}
|
|
4892
5640
|
structDone++;
|
|
4893
5641
|
context2.onPhaseProgress?.("structure", structDone, context2.filePaths.length);
|
|
@@ -4896,7 +5644,7 @@ var structurePhase = {
|
|
|
4896
5644
|
context2.graph.addNode({
|
|
4897
5645
|
id: generateNodeId("directory", dir, dir),
|
|
4898
5646
|
kind: "directory",
|
|
4899
|
-
name:
|
|
5647
|
+
name: path27.basename(dir),
|
|
4900
5648
|
filePath: dir
|
|
4901
5649
|
});
|
|
4902
5650
|
}
|
|
@@ -5053,7 +5801,7 @@ var LLMGovernanceLogger = class {
|
|
|
5053
5801
|
}
|
|
5054
5802
|
/** Path to the JSONL log file. */
|
|
5055
5803
|
getLogPath() {
|
|
5056
|
-
return process.env["CODE_INTEL_GOVERNANCE_LOG_PATH"] ??
|
|
5804
|
+
return process.env["CODE_INTEL_GOVERNANCE_LOG_PATH"] ?? path27.join(os12.homedir(), ".code-intel", "llm-governance.jsonl");
|
|
5057
5805
|
}
|
|
5058
5806
|
/**
|
|
5059
5807
|
* Append an entry to the governance log.
|
|
@@ -5069,7 +5817,7 @@ var LLMGovernanceLogger = class {
|
|
|
5069
5817
|
...entry
|
|
5070
5818
|
};
|
|
5071
5819
|
const logPath = this.getLogPath();
|
|
5072
|
-
fs19.mkdirSync(
|
|
5820
|
+
fs19.mkdirSync(path27.dirname(logPath), { recursive: true });
|
|
5073
5821
|
fs19.appendFileSync(logPath, JSON.stringify(full) + "\n", "utf-8");
|
|
5074
5822
|
} catch {
|
|
5075
5823
|
}
|
|
@@ -5525,7 +6273,7 @@ var DbManager = class {
|
|
|
5525
6273
|
this.dbPath = dbPath;
|
|
5526
6274
|
}
|
|
5527
6275
|
async init() {
|
|
5528
|
-
fs19.mkdirSync(
|
|
6276
|
+
fs19.mkdirSync(path27.dirname(this.dbPath), { recursive: true });
|
|
5529
6277
|
this.db = new Database(this.dbPath);
|
|
5530
6278
|
await this.db.init();
|
|
5531
6279
|
this.conn = new Connection(this.db);
|
|
@@ -5582,7 +6330,8 @@ var NODE_TABLE_MAP = {
|
|
|
5582
6330
|
constant: "const_nodes",
|
|
5583
6331
|
route: "route_nodes",
|
|
5584
6332
|
cluster: "cluster_nodes",
|
|
5585
|
-
flow: "flow_nodes"
|
|
6333
|
+
flow: "flow_nodes",
|
|
6334
|
+
vulnerability: "vuln_nodes"
|
|
5586
6335
|
};
|
|
5587
6336
|
var ALL_NODE_TABLES = [...new Set(Object.values(NODE_TABLE_MAP))];
|
|
5588
6337
|
function getCreateNodeTableDDL(tableName) {
|
|
@@ -5622,7 +6371,7 @@ function writeNodeCSVs(graph, outputDir) {
|
|
|
5622
6371
|
const table = NODE_TABLE_MAP[node.kind];
|
|
5623
6372
|
if (!tableBuffers.has(table)) {
|
|
5624
6373
|
tableBuffers.set(table, [header]);
|
|
5625
|
-
tableFilePaths.set(table,
|
|
6374
|
+
tableFilePaths.set(table, path27.join(outputDir, `${table}.csv`));
|
|
5626
6375
|
}
|
|
5627
6376
|
tableBuffers.get(table).push(
|
|
5628
6377
|
csvRow([
|
|
@@ -5658,7 +6407,7 @@ function writeEdgeCSV(graph, outputDir) {
|
|
|
5658
6407
|
const toTable = NODE_TABLE_MAP[targetNode.kind];
|
|
5659
6408
|
const key = `${fromTable}->${toTable}`;
|
|
5660
6409
|
if (!groups.has(key)) {
|
|
5661
|
-
const filePath =
|
|
6410
|
+
const filePath = path27.join(outputDir, `edges_${fromTable}_${toTable}.csv`);
|
|
5662
6411
|
groups.set(key, { lines: [header], from: fromTable, to: toTable, filePath });
|
|
5663
6412
|
}
|
|
5664
6413
|
groups.get(key).lines.push(
|
|
@@ -5701,7 +6450,7 @@ async function loadGraphToDB(graph, dbManager) {
|
|
|
5701
6450
|
} catch {
|
|
5702
6451
|
}
|
|
5703
6452
|
}
|
|
5704
|
-
const tmpDir = fs19.mkdtempSync(
|
|
6453
|
+
const tmpDir = fs19.mkdtempSync(path27.join(os12.tmpdir(), "code-intel-csv-"));
|
|
5705
6454
|
try {
|
|
5706
6455
|
const nodeTableFiles = writeNodeCSVs(graph, tmpDir);
|
|
5707
6456
|
const edgeGroups = writeEdgeCSV(graph, tmpDir);
|
|
@@ -5801,8 +6550,8 @@ function buildNodeProps(node) {
|
|
|
5801
6550
|
function escCypher(s) {
|
|
5802
6551
|
return s.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/\n/g, "\\n").replace(/\r/g, "");
|
|
5803
6552
|
}
|
|
5804
|
-
var GLOBAL_DIR =
|
|
5805
|
-
var REPOS_FILE =
|
|
6553
|
+
var GLOBAL_DIR = path27.join(os12.homedir(), ".code-intel");
|
|
6554
|
+
var REPOS_FILE = path27.join(GLOBAL_DIR, "repos.json");
|
|
5806
6555
|
function loadRegistry() {
|
|
5807
6556
|
try {
|
|
5808
6557
|
const data = fs19.readFileSync(REPOS_FILE, "utf-8");
|
|
@@ -5830,23 +6579,23 @@ function removeRepo(repoPath) {
|
|
|
5830
6579
|
saveRegistry(entries);
|
|
5831
6580
|
}
|
|
5832
6581
|
function saveMetadata(repoDir, metadata) {
|
|
5833
|
-
const metaDir =
|
|
6582
|
+
const metaDir = path27.join(repoDir, ".code-intel");
|
|
5834
6583
|
fs19.mkdirSync(metaDir, { recursive: true });
|
|
5835
|
-
fs19.writeFileSync(
|
|
6584
|
+
fs19.writeFileSync(path27.join(metaDir, "meta.json"), JSON.stringify(metadata, null, 2));
|
|
5836
6585
|
}
|
|
5837
6586
|
function loadMetadata(repoDir) {
|
|
5838
6587
|
try {
|
|
5839
|
-
const data = fs19.readFileSync(
|
|
6588
|
+
const data = fs19.readFileSync(path27.join(repoDir, ".code-intel", "meta.json"), "utf-8");
|
|
5840
6589
|
return JSON.parse(data);
|
|
5841
6590
|
} catch {
|
|
5842
6591
|
return null;
|
|
5843
6592
|
}
|
|
5844
6593
|
}
|
|
5845
6594
|
function getDbPath(repoDir) {
|
|
5846
|
-
return
|
|
6595
|
+
return path27.join(repoDir, ".code-intel", "graph.db");
|
|
5847
6596
|
}
|
|
5848
6597
|
function getVectorDbPath(repoDir) {
|
|
5849
|
-
return
|
|
6598
|
+
return path27.join(repoDir, ".code-intel", "vector.db");
|
|
5850
6599
|
}
|
|
5851
6600
|
|
|
5852
6601
|
// src/mcp-server/server.ts
|
|
@@ -6045,7 +6794,7 @@ async function syncGroup(group) {
|
|
|
6045
6794
|
logger_default.warn(` \u26A0 Registry entry "${member.registryName}" not found \u2014 skipping ${member.groupPath}`);
|
|
6046
6795
|
continue;
|
|
6047
6796
|
}
|
|
6048
|
-
const dbPath =
|
|
6797
|
+
const dbPath = path27.join(regEntry.path, ".code-intel", "graph.db");
|
|
6049
6798
|
if (!fs19.existsSync(dbPath)) {
|
|
6050
6799
|
logger_default.warn(` \u26A0 No index at ${dbPath} \u2014 run \`code-intel analyze ${regEntry.path}\` first`);
|
|
6051
6800
|
continue;
|
|
@@ -6081,7 +6830,7 @@ async function queryGroup(group, query, limit = 20) {
|
|
|
6081
6830
|
for (const member of group.members) {
|
|
6082
6831
|
const regEntry = registry.find((r) => r.name === member.registryName);
|
|
6083
6832
|
if (!regEntry) continue;
|
|
6084
|
-
const dbPath =
|
|
6833
|
+
const dbPath = path27.join(regEntry.path, ".code-intel", "graph.db");
|
|
6085
6834
|
if (!fs19.existsSync(dbPath)) continue;
|
|
6086
6835
|
const graph = createKnowledgeGraph();
|
|
6087
6836
|
const db = new DbManager(dbPath);
|
|
@@ -6112,65 +6861,684 @@ async function queryGroup(group, query, limit = 20) {
|
|
|
6112
6861
|
|
|
6113
6862
|
// src/mcp-server/server.ts
|
|
6114
6863
|
init_tracing();
|
|
6115
|
-
|
|
6116
|
-
|
|
6117
|
-
|
|
6118
|
-
|
|
6119
|
-
);
|
|
6120
|
-
|
|
6121
|
-
|
|
6122
|
-
|
|
6123
|
-
|
|
6124
|
-
|
|
6125
|
-
|
|
6126
|
-
|
|
6127
|
-
|
|
6128
|
-
|
|
6129
|
-
|
|
6130
|
-
|
|
6131
|
-
|
|
6132
|
-
|
|
6133
|
-
|
|
6134
|
-
|
|
6135
|
-
|
|
6136
|
-
|
|
6137
|
-
|
|
6138
|
-
|
|
6139
|
-
|
|
6140
|
-
|
|
6141
|
-
|
|
6142
|
-
|
|
6143
|
-
|
|
6144
|
-
|
|
6145
|
-
|
|
6146
|
-
|
|
6147
|
-
|
|
6864
|
+
|
|
6865
|
+
// src/query/explain-relationship.ts
|
|
6866
|
+
function explainRelationship(graph, from, to) {
|
|
6867
|
+
const allNodes = [...graph.allNodes()];
|
|
6868
|
+
const fromNode = allNodes.find((n) => n.name === from);
|
|
6869
|
+
if (!fromNode) {
|
|
6870
|
+
const firstChar = from[0]?.toLowerCase() ?? "";
|
|
6871
|
+
const fromLower = from.toLowerCase();
|
|
6872
|
+
const suggestions = allNodes.filter((n) => n.name.toLowerCase().startsWith(firstChar) || n.name.toLowerCase().includes(fromLower)).slice(0, 5).map((n) => n.name);
|
|
6873
|
+
return { error: `Symbol not found: ${from}`, suggestions };
|
|
6874
|
+
}
|
|
6875
|
+
const toNode = allNodes.find((n) => n.name === to);
|
|
6876
|
+
if (!toNode) {
|
|
6877
|
+
const firstChar = to[0]?.toLowerCase() ?? "";
|
|
6878
|
+
const toLower = to.toLowerCase();
|
|
6879
|
+
const suggestions = allNodes.filter((n) => n.name.toLowerCase().startsWith(firstChar) || n.name.toLowerCase().includes(toLower)).slice(0, 5).map((n) => n.name);
|
|
6880
|
+
return { error: `Symbol not found: ${to}`, suggestions };
|
|
6881
|
+
}
|
|
6882
|
+
const paths = [];
|
|
6883
|
+
const queue = [{
|
|
6884
|
+
id: fromNode.id,
|
|
6885
|
+
nodeNames: [fromNode.name],
|
|
6886
|
+
lastEdgeKind: "",
|
|
6887
|
+
visited: /* @__PURE__ */ new Set([fromNode.id])
|
|
6888
|
+
}];
|
|
6889
|
+
while (queue.length > 0 && paths.length < 10) {
|
|
6890
|
+
const entry = queue.shift();
|
|
6891
|
+
const { id, nodeNames, visited } = entry;
|
|
6892
|
+
if (nodeNames.length > 6) continue;
|
|
6893
|
+
for (const edge of graph.findEdgesFrom(id)) {
|
|
6894
|
+
const targetNode = graph.getNode(edge.target);
|
|
6895
|
+
if (!targetNode) continue;
|
|
6896
|
+
if (visited.has(edge.target)) continue;
|
|
6897
|
+
const newNames = [...nodeNames, targetNode.name];
|
|
6898
|
+
if (edge.target === toNode.id) {
|
|
6899
|
+
paths.push({ hops: newNames.length - 1, nodes: newNames, edgeKind: edge.kind });
|
|
6900
|
+
if (paths.length >= 10) break;
|
|
6901
|
+
continue;
|
|
6902
|
+
}
|
|
6903
|
+
if (newNames.length < 6) {
|
|
6904
|
+
const newVisited = new Set(visited);
|
|
6905
|
+
newVisited.add(edge.target);
|
|
6906
|
+
queue.push({ id: edge.target, nodeNames: newNames, lastEdgeKind: edge.kind, visited: newVisited });
|
|
6907
|
+
}
|
|
6908
|
+
}
|
|
6909
|
+
}
|
|
6910
|
+
const fromImports = /* @__PURE__ */ new Set();
|
|
6911
|
+
for (const edge of graph.findEdgesFrom(fromNode.id)) {
|
|
6912
|
+
if (edge.kind === "imports") fromImports.add(edge.target);
|
|
6913
|
+
}
|
|
6914
|
+
const sharedImportIds = [];
|
|
6915
|
+
for (const edge of graph.findEdgesFrom(toNode.id)) {
|
|
6916
|
+
if (edge.kind === "imports" && fromImports.has(edge.target)) {
|
|
6917
|
+
sharedImportIds.push(edge.target);
|
|
6918
|
+
}
|
|
6919
|
+
}
|
|
6920
|
+
const sharedImports = sharedImportIds.map((id) => graph.getNode(id)?.name ?? id);
|
|
6921
|
+
let heritage = null;
|
|
6922
|
+
for (const edge of graph.findEdgesFrom(fromNode.id)) {
|
|
6923
|
+
if ((edge.kind === "extends" || edge.kind === "implements") && edge.target === toNode.id) {
|
|
6924
|
+
heritage = `${from} ${edge.kind} ${to}`;
|
|
6925
|
+
break;
|
|
6926
|
+
}
|
|
6927
|
+
}
|
|
6928
|
+
if (!heritage) {
|
|
6929
|
+
for (const edge of graph.findEdgesFrom(toNode.id)) {
|
|
6930
|
+
if ((edge.kind === "extends" || edge.kind === "implements") && edge.target === fromNode.id) {
|
|
6931
|
+
heritage = `${to} ${edge.kind} ${from}`;
|
|
6932
|
+
break;
|
|
6933
|
+
}
|
|
6934
|
+
}
|
|
6935
|
+
}
|
|
6936
|
+
const sharedStr = sharedImports.length > 0 ? sharedImports.join(", ") : "none";
|
|
6937
|
+
const heritageStr = heritage ?? "none";
|
|
6938
|
+
const connectionStr = paths.length === 0 ? "No connection found." : `${from} \u2192 ${to} via ${paths.length} path(s).`;
|
|
6939
|
+
const summary = `${connectionStr} Shared imports: [${sharedStr}]. Heritage: ${heritageStr}.`;
|
|
6940
|
+
return { paths, sharedImports, heritage, summary };
|
|
6941
|
+
}
|
|
6942
|
+
|
|
6943
|
+
// src/query/pr-impact.ts
|
|
6944
|
+
function parseDiffFiles(diff) {
|
|
6945
|
+
const files = [];
|
|
6946
|
+
for (const line of diff.split("\n")) {
|
|
6947
|
+
const match = line.match(/^\+\+\+ b\/(.+)/);
|
|
6948
|
+
if (match) {
|
|
6949
|
+
files.push(match[1]);
|
|
6950
|
+
}
|
|
6951
|
+
}
|
|
6952
|
+
return files;
|
|
6953
|
+
}
|
|
6954
|
+
function computePRImpact(graph, changedFiles, maxHops) {
|
|
6955
|
+
const changedSymbolIds = /* @__PURE__ */ new Set();
|
|
6956
|
+
for (const node of graph.allNodes()) {
|
|
6957
|
+
if (!node.filePath) continue;
|
|
6958
|
+
for (const changedFile of changedFiles) {
|
|
6959
|
+
if (node.filePath === changedFile || node.filePath.endsWith(changedFile) || changedFile.endsWith(node.filePath)) {
|
|
6960
|
+
changedSymbolIds.add(node.id);
|
|
6961
|
+
break;
|
|
6962
|
+
}
|
|
6963
|
+
}
|
|
6964
|
+
}
|
|
6965
|
+
const allBlastRadiusNodes = /* @__PURE__ */ new Set();
|
|
6966
|
+
const changedSymbols = [];
|
|
6967
|
+
for (const symbolId of changedSymbolIds) {
|
|
6968
|
+
const symbolNode = graph.getNode(symbolId);
|
|
6969
|
+
if (!symbolNode) continue;
|
|
6970
|
+
const blastRadius = /* @__PURE__ */ new Set();
|
|
6971
|
+
const queue = [{ id: symbolId, depth: 0 }];
|
|
6972
|
+
const visited = /* @__PURE__ */ new Set();
|
|
6973
|
+
while (queue.length > 0) {
|
|
6974
|
+
const { id, depth } = queue.shift();
|
|
6975
|
+
if (visited.has(id) || depth > maxHops) continue;
|
|
6976
|
+
visited.add(id);
|
|
6977
|
+
if (id !== symbolId) blastRadius.add(id);
|
|
6978
|
+
for (const edge of graph.findEdgesTo(id)) {
|
|
6979
|
+
if (edge.kind === "calls" || edge.kind === "imports") {
|
|
6980
|
+
queue.push({ id: edge.source, depth: depth + 1 });
|
|
6148
6981
|
}
|
|
6149
|
-
}
|
|
6150
|
-
|
|
6151
|
-
|
|
6152
|
-
|
|
6153
|
-
|
|
6154
|
-
|
|
6155
|
-
|
|
6156
|
-
|
|
6157
|
-
|
|
6158
|
-
|
|
6159
|
-
|
|
6982
|
+
}
|
|
6983
|
+
}
|
|
6984
|
+
for (const id of blastRadius) allBlastRadiusNodes.add(id);
|
|
6985
|
+
const blastCount = blastRadius.size;
|
|
6986
|
+
let risk;
|
|
6987
|
+
if (blastCount > 50) {
|
|
6988
|
+
risk = "HIGH";
|
|
6989
|
+
} else if (blastCount >= 10) {
|
|
6990
|
+
risk = "MEDIUM";
|
|
6991
|
+
} else {
|
|
6992
|
+
risk = "LOW";
|
|
6993
|
+
}
|
|
6994
|
+
let callerCount = 0;
|
|
6995
|
+
for (const edge of graph.findEdgesTo(symbolId)) {
|
|
6996
|
+
if (edge.kind === "calls") callerCount++;
|
|
6997
|
+
}
|
|
6998
|
+
let testCoverage = false;
|
|
6999
|
+
for (const edge of graph.findEdgesTo(symbolId)) {
|
|
7000
|
+
if (edge.kind === "imports") {
|
|
7001
|
+
const callerNode = graph.getNode(edge.source);
|
|
7002
|
+
if (callerNode?.filePath && (callerNode.filePath.includes(".test.") || callerNode.filePath.includes(".spec."))) {
|
|
7003
|
+
testCoverage = true;
|
|
7004
|
+
break;
|
|
6160
7005
|
}
|
|
6161
|
-
}
|
|
6162
|
-
|
|
6163
|
-
|
|
6164
|
-
|
|
6165
|
-
|
|
6166
|
-
|
|
6167
|
-
|
|
6168
|
-
|
|
6169
|
-
|
|
6170
|
-
|
|
6171
|
-
|
|
6172
|
-
|
|
6173
|
-
|
|
7006
|
+
}
|
|
7007
|
+
}
|
|
7008
|
+
changedSymbols.push({ name: symbolNode.name, risk, callerCount, testCoverage });
|
|
7009
|
+
}
|
|
7010
|
+
const impactedSymbols = [];
|
|
7011
|
+
for (const id of allBlastRadiusNodes) {
|
|
7012
|
+
if (changedSymbolIds.has(id)) continue;
|
|
7013
|
+
const node = graph.getNode(id);
|
|
7014
|
+
if (node) {
|
|
7015
|
+
impactedSymbols.push({ name: node.name, filePath: node.filePath });
|
|
7016
|
+
}
|
|
7017
|
+
}
|
|
7018
|
+
const riskSummary = { HIGH: 0, MEDIUM: 0, LOW: 0 };
|
|
7019
|
+
for (const s of changedSymbols) {
|
|
7020
|
+
riskSummary[s.risk]++;
|
|
7021
|
+
}
|
|
7022
|
+
const coverageGaps = [];
|
|
7023
|
+
for (const s of changedSymbols) {
|
|
7024
|
+
if ((s.risk === "HIGH" || s.risk === "MEDIUM") && !s.testCoverage) {
|
|
7025
|
+
coverageGaps.push(`${s.name} has no test coverage`);
|
|
7026
|
+
}
|
|
7027
|
+
}
|
|
7028
|
+
const fileImpactCount = /* @__PURE__ */ new Map();
|
|
7029
|
+
for (const sym of impactedSymbols) {
|
|
7030
|
+
if (sym.filePath) {
|
|
7031
|
+
fileImpactCount.set(sym.filePath, (fileImpactCount.get(sym.filePath) ?? 0) + 1);
|
|
7032
|
+
}
|
|
7033
|
+
}
|
|
7034
|
+
const filesToReview = [...fileImpactCount.entries()].sort((a, b) => b[1] - a[1]).slice(0, 5).map(([fp]) => fp);
|
|
7035
|
+
return {
|
|
7036
|
+
changedSymbols,
|
|
7037
|
+
impactedSymbols,
|
|
7038
|
+
riskSummary,
|
|
7039
|
+
coverageGaps,
|
|
7040
|
+
filesToReview,
|
|
7041
|
+
crossRepoImpact: null
|
|
7042
|
+
};
|
|
7043
|
+
}
|
|
7044
|
+
|
|
7045
|
+
// src/query/similar-symbols.ts
|
|
7046
|
+
function levenshtein(a, b) {
|
|
7047
|
+
const m = a.length;
|
|
7048
|
+
const n = b.length;
|
|
7049
|
+
const dp = Array.from(
|
|
7050
|
+
{ length: m + 1 },
|
|
7051
|
+
(_, i) => Array.from({ length: n + 1 }, (_2, j) => i === 0 ? j : j === 0 ? i : 0)
|
|
7052
|
+
);
|
|
7053
|
+
for (let i = 1; i <= m; i++) {
|
|
7054
|
+
for (let j = 1; j <= n; j++) {
|
|
7055
|
+
if (a[i - 1] === b[j - 1]) {
|
|
7056
|
+
dp[i][j] = dp[i - 1][j - 1];
|
|
7057
|
+
} else {
|
|
7058
|
+
dp[i][j] = 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
|
|
7059
|
+
}
|
|
7060
|
+
}
|
|
7061
|
+
}
|
|
7062
|
+
return dp[m][n];
|
|
7063
|
+
}
|
|
7064
|
+
function findSimilarSymbols(graph, symbolName, limit) {
|
|
7065
|
+
const clampedLimit = Math.min(Math.max(1, limit), 50);
|
|
7066
|
+
const allNodes = [...graph.allNodes()];
|
|
7067
|
+
const targetNode = allNodes.find((n) => n.name === symbolName);
|
|
7068
|
+
if (!targetNode) {
|
|
7069
|
+
return { similar: [] };
|
|
7070
|
+
}
|
|
7071
|
+
let targetCluster = null;
|
|
7072
|
+
for (const edge of graph.findEdgesFrom(targetNode.id)) {
|
|
7073
|
+
if (edge.kind === "belongs_to") {
|
|
7074
|
+
const clusterNode = graph.getNode(edge.target);
|
|
7075
|
+
if (clusterNode) {
|
|
7076
|
+
targetCluster = clusterNode.name;
|
|
7077
|
+
break;
|
|
7078
|
+
}
|
|
7079
|
+
}
|
|
7080
|
+
}
|
|
7081
|
+
if (!targetCluster) {
|
|
7082
|
+
for (const edge of graph.findEdgesTo(targetNode.id)) {
|
|
7083
|
+
if (edge.kind === "belongs_to") {
|
|
7084
|
+
const clusterNode = graph.getNode(edge.source);
|
|
7085
|
+
if (clusterNode) {
|
|
7086
|
+
targetCluster = clusterNode.name;
|
|
7087
|
+
break;
|
|
7088
|
+
}
|
|
7089
|
+
}
|
|
7090
|
+
}
|
|
7091
|
+
}
|
|
7092
|
+
const results = [];
|
|
7093
|
+
for (const node of allNodes) {
|
|
7094
|
+
if (node.id === targetNode.id) continue;
|
|
7095
|
+
const maxLen = Math.max(symbolName.length, node.name.length);
|
|
7096
|
+
const nameSim = maxLen === 0 ? 1 : 1 - levenshtein(symbolName, node.name) / maxLen;
|
|
7097
|
+
const structuralSim = node.kind === targetNode.kind ? 0.5 : 0;
|
|
7098
|
+
const combined = 0.5 * nameSim + 0.5 * structuralSim;
|
|
7099
|
+
const reasons = [];
|
|
7100
|
+
if (nameSim >= 0.6) reasons.push("similar name");
|
|
7101
|
+
if (node.kind === targetNode.kind) reasons.push("same kind");
|
|
7102
|
+
if (targetCluster !== null) {
|
|
7103
|
+
let nodeCluster = null;
|
|
7104
|
+
for (const edge of graph.findEdgesFrom(node.id)) {
|
|
7105
|
+
if (edge.kind === "belongs_to") {
|
|
7106
|
+
const clusterNode = graph.getNode(edge.target);
|
|
7107
|
+
if (clusterNode) {
|
|
7108
|
+
nodeCluster = clusterNode.name;
|
|
7109
|
+
break;
|
|
7110
|
+
}
|
|
7111
|
+
}
|
|
7112
|
+
}
|
|
7113
|
+
if (!nodeCluster) {
|
|
7114
|
+
for (const edge of graph.findEdgesTo(node.id)) {
|
|
7115
|
+
if (edge.kind === "belongs_to") {
|
|
7116
|
+
const clusterNode = graph.getNode(edge.source);
|
|
7117
|
+
if (clusterNode) {
|
|
7118
|
+
nodeCluster = clusterNode.name;
|
|
7119
|
+
break;
|
|
7120
|
+
}
|
|
7121
|
+
}
|
|
7122
|
+
}
|
|
7123
|
+
}
|
|
7124
|
+
if (nodeCluster !== null && nodeCluster === targetCluster) {
|
|
7125
|
+
reasons.push("same module");
|
|
7126
|
+
}
|
|
7127
|
+
}
|
|
7128
|
+
if (targetNode.metadata?.["cluster"] !== void 0 && node.metadata?.["cluster"] !== void 0 && node.metadata["cluster"] === targetNode.metadata["cluster"]) {
|
|
7129
|
+
if (!reasons.includes("same module")) reasons.push("same module");
|
|
7130
|
+
}
|
|
7131
|
+
results.push({ name: node.name, similarity: combined, reasons });
|
|
7132
|
+
}
|
|
7133
|
+
results.sort((a, b) => b.similarity - a.similarity);
|
|
7134
|
+
return { similar: results.slice(0, clampedLimit) };
|
|
7135
|
+
}
|
|
7136
|
+
|
|
7137
|
+
// src/query/health-report.ts
|
|
7138
|
+
function computeHealthReport(graph, scope) {
|
|
7139
|
+
const wholeRepo = scope === ".";
|
|
7140
|
+
function inScope(filePath) {
|
|
7141
|
+
if (wholeRepo) return true;
|
|
7142
|
+
return filePath.startsWith(scope) || filePath.includes(scope);
|
|
7143
|
+
}
|
|
7144
|
+
const scopedNodes = [...graph.allNodes()].filter((n) => inScope(n.filePath));
|
|
7145
|
+
const deadCodeKinds = /* @__PURE__ */ new Set(["function", "method", "class"]);
|
|
7146
|
+
const deadCode = [];
|
|
7147
|
+
for (const node of scopedNodes) {
|
|
7148
|
+
if (!deadCodeKinds.has(node.kind)) continue;
|
|
7149
|
+
if (node.exported === true) continue;
|
|
7150
|
+
let hasIncoming = false;
|
|
7151
|
+
for (const _edge of graph.findEdgesTo(node.id)) {
|
|
7152
|
+
hasIncoming = true;
|
|
7153
|
+
break;
|
|
7154
|
+
}
|
|
7155
|
+
if (!hasIncoming) {
|
|
7156
|
+
deadCode.push({ name: node.name, filePath: node.filePath, kind: node.kind });
|
|
7157
|
+
if (deadCode.length >= 20) break;
|
|
7158
|
+
}
|
|
7159
|
+
}
|
|
7160
|
+
const cycles = [];
|
|
7161
|
+
const scopedNodeIds = new Set(scopedNodes.map((n) => n.id));
|
|
7162
|
+
const importAdj = /* @__PURE__ */ new Map();
|
|
7163
|
+
for (const node of scopedNodes) {
|
|
7164
|
+
importAdj.set(node.id, []);
|
|
7165
|
+
}
|
|
7166
|
+
for (const edge of graph.findEdgesByKind("imports")) {
|
|
7167
|
+
if (scopedNodeIds.has(edge.source) && scopedNodeIds.has(edge.target)) {
|
|
7168
|
+
importAdj.get(edge.source).push(edge.target);
|
|
7169
|
+
}
|
|
7170
|
+
}
|
|
7171
|
+
const visited = /* @__PURE__ */ new Set();
|
|
7172
|
+
const inStack = /* @__PURE__ */ new Set();
|
|
7173
|
+
const stackPath = [];
|
|
7174
|
+
function dfs(nodeId) {
|
|
7175
|
+
if (cycles.length >= 5) return;
|
|
7176
|
+
visited.add(nodeId);
|
|
7177
|
+
inStack.add(nodeId);
|
|
7178
|
+
stackPath.push(nodeId);
|
|
7179
|
+
for (const neighborId of importAdj.get(nodeId) ?? []) {
|
|
7180
|
+
if (cycles.length >= 5) break;
|
|
7181
|
+
if (inStack.has(neighborId)) {
|
|
7182
|
+
const cycleStart = stackPath.indexOf(neighborId);
|
|
7183
|
+
const cyclePath = stackPath.slice(cycleStart).map((id) => {
|
|
7184
|
+
const node = graph.getNode(id);
|
|
7185
|
+
return node ? node.name : id;
|
|
7186
|
+
});
|
|
7187
|
+
cycles.push(cyclePath);
|
|
7188
|
+
} else if (!visited.has(neighborId)) {
|
|
7189
|
+
dfs(neighborId);
|
|
7190
|
+
}
|
|
7191
|
+
}
|
|
7192
|
+
stackPath.pop();
|
|
7193
|
+
inStack.delete(nodeId);
|
|
7194
|
+
}
|
|
7195
|
+
for (const node of scopedNodes) {
|
|
7196
|
+
if (cycles.length >= 5) break;
|
|
7197
|
+
if (!visited.has(node.id)) {
|
|
7198
|
+
dfs(node.id);
|
|
7199
|
+
}
|
|
7200
|
+
}
|
|
7201
|
+
const godNodes = [];
|
|
7202
|
+
for (const node of scopedNodes) {
|
|
7203
|
+
let edgeCount = 0;
|
|
7204
|
+
for (const _edge of graph.findEdgesFrom(node.id)) {
|
|
7205
|
+
edgeCount++;
|
|
7206
|
+
}
|
|
7207
|
+
if (edgeCount > 10) {
|
|
7208
|
+
godNodes.push({ name: node.name, edgeCount, filePath: node.filePath });
|
|
7209
|
+
}
|
|
7210
|
+
}
|
|
7211
|
+
godNodes.sort((a, b) => b.edgeCount - a.edgeCount);
|
|
7212
|
+
godNodes.splice(10);
|
|
7213
|
+
const filePathToNodes = /* @__PURE__ */ new Map();
|
|
7214
|
+
for (const node of scopedNodes) {
|
|
7215
|
+
if (!node.filePath) continue;
|
|
7216
|
+
let arr = filePathToNodes.get(node.filePath);
|
|
7217
|
+
if (!arr) {
|
|
7218
|
+
arr = [];
|
|
7219
|
+
filePathToNodes.set(node.filePath, arr);
|
|
7220
|
+
}
|
|
7221
|
+
arr.push(node.id);
|
|
7222
|
+
}
|
|
7223
|
+
const orphanFiles = [];
|
|
7224
|
+
for (const [filePath, nodeIds] of filePathToNodes) {
|
|
7225
|
+
if (orphanFiles.length >= 10) break;
|
|
7226
|
+
let hasAnyEdge = false;
|
|
7227
|
+
for (const nodeId of nodeIds) {
|
|
7228
|
+
let hasOut = false;
|
|
7229
|
+
for (const _edge of graph.findEdgesFrom(nodeId)) {
|
|
7230
|
+
hasOut = true;
|
|
7231
|
+
break;
|
|
7232
|
+
}
|
|
7233
|
+
let hasIn = false;
|
|
7234
|
+
for (const _edge of graph.findEdgesTo(nodeId)) {
|
|
7235
|
+
hasIn = true;
|
|
7236
|
+
break;
|
|
7237
|
+
}
|
|
7238
|
+
if (hasOut || hasIn) {
|
|
7239
|
+
hasAnyEdge = true;
|
|
7240
|
+
break;
|
|
7241
|
+
}
|
|
7242
|
+
}
|
|
7243
|
+
if (!hasAnyEdge) {
|
|
7244
|
+
orphanFiles.push(filePath);
|
|
7245
|
+
}
|
|
7246
|
+
}
|
|
7247
|
+
const hotspotCandidates = [];
|
|
7248
|
+
for (const node of scopedNodes) {
|
|
7249
|
+
const visitedBfs = /* @__PURE__ */ new Set();
|
|
7250
|
+
const queue = [{ id: node.id, depth: 0 }];
|
|
7251
|
+
while (queue.length > 0) {
|
|
7252
|
+
const item = queue.shift();
|
|
7253
|
+
if (item.depth > 5 || visitedBfs.has(item.id)) continue;
|
|
7254
|
+
visitedBfs.add(item.id);
|
|
7255
|
+
for (const edge of graph.findEdgesTo(item.id)) {
|
|
7256
|
+
if (edge.kind === "calls" || edge.kind === "imports") {
|
|
7257
|
+
if (!visitedBfs.has(edge.source)) {
|
|
7258
|
+
queue.push({ id: edge.source, depth: item.depth + 1 });
|
|
7259
|
+
}
|
|
7260
|
+
}
|
|
7261
|
+
}
|
|
7262
|
+
}
|
|
7263
|
+
const blastRadius = visitedBfs.size - 1;
|
|
7264
|
+
hotspotCandidates.push({ name: node.name, blastRadius, filePath: node.filePath });
|
|
7265
|
+
}
|
|
7266
|
+
hotspotCandidates.sort((a, b) => b.blastRadius - a.blastRadius);
|
|
7267
|
+
const complexityHotspots = hotspotCandidates.slice(0, 5);
|
|
7268
|
+
const healthScore = Math.max(
|
|
7269
|
+
0,
|
|
7270
|
+
Math.min(100, 100 - deadCode.length * 2 - cycles.length * 5 - godNodes.length * 3)
|
|
7271
|
+
);
|
|
7272
|
+
return {
|
|
7273
|
+
healthScore,
|
|
7274
|
+
deadCode,
|
|
7275
|
+
cycles,
|
|
7276
|
+
godNodes,
|
|
7277
|
+
orphanFiles,
|
|
7278
|
+
complexityHotspots
|
|
7279
|
+
};
|
|
7280
|
+
}
|
|
7281
|
+
|
|
7282
|
+
// src/query/suggest-tests.ts
|
|
7283
|
+
function getSuggestedCases(symbolName) {
|
|
7284
|
+
const lower = symbolName.toLowerCase();
|
|
7285
|
+
if (/parse|validate|check|verify/.test(lower)) {
|
|
7286
|
+
return [
|
|
7287
|
+
"Valid input \u2192 success",
|
|
7288
|
+
"Invalid input \u2192 throws error",
|
|
7289
|
+
"Edge case: empty/null input \u2192 handled gracefully"
|
|
7290
|
+
];
|
|
7291
|
+
}
|
|
7292
|
+
if (/create|add|insert|save/.test(lower)) {
|
|
7293
|
+
return [
|
|
7294
|
+
"Success: valid data \u2192 created",
|
|
7295
|
+
"Duplicate: existing item \u2192 error or no-op",
|
|
7296
|
+
"Missing required fields \u2192 validation error"
|
|
7297
|
+
];
|
|
7298
|
+
}
|
|
7299
|
+
if (/delete|remove|destroy/.test(lower)) {
|
|
7300
|
+
return [
|
|
7301
|
+
"Existing item \u2192 deleted successfully",
|
|
7302
|
+
"Non-existent item \u2192 no error or 404",
|
|
7303
|
+
"Unauthorized access \u2192 rejected"
|
|
7304
|
+
];
|
|
7305
|
+
}
|
|
7306
|
+
if (/get|find|fetch|load/.test(lower)) {
|
|
7307
|
+
return [
|
|
7308
|
+
"Found: returns correct data",
|
|
7309
|
+
"Not found: returns null or throws",
|
|
7310
|
+
"Empty collection: returns []"
|
|
7311
|
+
];
|
|
7312
|
+
}
|
|
7313
|
+
return [
|
|
7314
|
+
"Happy path: valid input \u2192 expected output",
|
|
7315
|
+
"Error case: invalid input \u2192 error handled",
|
|
7316
|
+
"Edge case: boundary values \u2192 correct behavior"
|
|
7317
|
+
];
|
|
7318
|
+
}
|
|
7319
|
+
function suggestTests(graph, symbolName) {
|
|
7320
|
+
let targetNode = void 0;
|
|
7321
|
+
for (const node of graph.allNodes()) {
|
|
7322
|
+
if (node.name === symbolName) {
|
|
7323
|
+
targetNode = node;
|
|
7324
|
+
break;
|
|
7325
|
+
}
|
|
7326
|
+
}
|
|
7327
|
+
if (!targetNode) {
|
|
7328
|
+
return { error: `Symbol not found: ${symbolName}` };
|
|
7329
|
+
}
|
|
7330
|
+
const targetId = targetNode.id;
|
|
7331
|
+
const callPaths = [];
|
|
7332
|
+
const pathQueue = [{ id: targetId, path: [symbolName], depth: 0 }];
|
|
7333
|
+
while (pathQueue.length > 0 && callPaths.length < 5) {
|
|
7334
|
+
const { id, path: path28, depth } = pathQueue.shift();
|
|
7335
|
+
let hasCallers = false;
|
|
7336
|
+
for (const edge of graph.findEdgesTo(id)) {
|
|
7337
|
+
if (edge.kind !== "calls") continue;
|
|
7338
|
+
const callerNode = graph.getNode(edge.source);
|
|
7339
|
+
if (!callerNode) continue;
|
|
7340
|
+
hasCallers = true;
|
|
7341
|
+
const newPath = [callerNode.name, ...path28];
|
|
7342
|
+
if (depth + 1 >= 3 || callPaths.length >= 5) {
|
|
7343
|
+
if (callPaths.length < 5) callPaths.push(newPath);
|
|
7344
|
+
continue;
|
|
7345
|
+
}
|
|
7346
|
+
pathQueue.push({ id: edge.source, path: newPath, depth: depth + 1 });
|
|
7347
|
+
}
|
|
7348
|
+
if (!hasCallers && path28.length > 1) {
|
|
7349
|
+
callPaths.push(path28);
|
|
7350
|
+
}
|
|
7351
|
+
}
|
|
7352
|
+
if (callPaths.length === 0) {
|
|
7353
|
+
for (const edge of graph.findEdgesTo(targetId)) {
|
|
7354
|
+
if (edge.kind !== "calls") continue;
|
|
7355
|
+
const callerNode = graph.getNode(edge.source);
|
|
7356
|
+
if (!callerNode) continue;
|
|
7357
|
+
callPaths.push([callerNode.name, symbolName]);
|
|
7358
|
+
if (callPaths.length >= 5) break;
|
|
7359
|
+
}
|
|
7360
|
+
}
|
|
7361
|
+
const existingTestFiles = /* @__PURE__ */ new Set();
|
|
7362
|
+
for (const edge of graph.findEdgesTo(targetId)) {
|
|
7363
|
+
if (edge.kind !== "imports") continue;
|
|
7364
|
+
const importerNode = graph.getNode(edge.source);
|
|
7365
|
+
if (!importerNode) continue;
|
|
7366
|
+
if (importerNode.filePath.includes(".test.") || importerNode.filePath.includes(".spec.")) {
|
|
7367
|
+
existingTestFiles.add(importerNode.filePath);
|
|
7368
|
+
}
|
|
7369
|
+
}
|
|
7370
|
+
const existingTests = [...existingTestFiles];
|
|
7371
|
+
const untestedCallers = [];
|
|
7372
|
+
for (const edge of graph.findEdgesTo(targetId)) {
|
|
7373
|
+
if (edge.kind !== "calls") continue;
|
|
7374
|
+
const callerNode = graph.getNode(edge.source);
|
|
7375
|
+
if (!callerNode) continue;
|
|
7376
|
+
if (callerNode.filePath.includes(".test.") || callerNode.filePath.includes(".spec.")) {
|
|
7377
|
+
continue;
|
|
7378
|
+
}
|
|
7379
|
+
let callerHasTest = false;
|
|
7380
|
+
for (const callerImportEdge of graph.findEdgesTo(callerNode.id)) {
|
|
7381
|
+
if (callerImportEdge.kind !== "imports") continue;
|
|
7382
|
+
const importerOfCaller = graph.getNode(callerImportEdge.source);
|
|
7383
|
+
if (!importerOfCaller) continue;
|
|
7384
|
+
if (importerOfCaller.filePath.includes(".test.") || importerOfCaller.filePath.includes(".spec.")) {
|
|
7385
|
+
callerHasTest = true;
|
|
7386
|
+
break;
|
|
7387
|
+
}
|
|
7388
|
+
}
|
|
7389
|
+
if (!callerHasTest) {
|
|
7390
|
+
untestedCallers.push(callerNode.name);
|
|
7391
|
+
}
|
|
7392
|
+
}
|
|
7393
|
+
const suggestedCases = getSuggestedCases(symbolName);
|
|
7394
|
+
return {
|
|
7395
|
+
callPaths,
|
|
7396
|
+
suggestedCases,
|
|
7397
|
+
existingTests,
|
|
7398
|
+
untestedCallers
|
|
7399
|
+
};
|
|
7400
|
+
}
|
|
7401
|
+
|
|
7402
|
+
// src/query/cluster-summary.ts
|
|
7403
|
+
function getPathPrefix(filePath) {
|
|
7404
|
+
const parts = filePath.replace(/\\/g, "/").split("/");
|
|
7405
|
+
return parts.slice(0, 2).join("/");
|
|
7406
|
+
}
|
|
7407
|
+
function summarizeCluster(graph, cluster) {
|
|
7408
|
+
const clusterNodes = [...graph.allNodes()].filter(
|
|
7409
|
+
(n) => n.filePath.startsWith(cluster) || n.metadata?.["cluster"] === cluster
|
|
7410
|
+
);
|
|
7411
|
+
if (clusterNodes.length === 0) {
|
|
7412
|
+
return { error: `Cluster not found: ${cluster}` };
|
|
7413
|
+
}
|
|
7414
|
+
const clusterNodeIds = new Set(clusterNodes.map((n) => n.id));
|
|
7415
|
+
const callerCountMap = /* @__PURE__ */ new Map();
|
|
7416
|
+
for (const node of clusterNodes) {
|
|
7417
|
+
let count = 0;
|
|
7418
|
+
for (const _edge of graph.findEdgesTo(node.id)) {
|
|
7419
|
+
count++;
|
|
7420
|
+
}
|
|
7421
|
+
callerCountMap.set(node.id, count);
|
|
7422
|
+
}
|
|
7423
|
+
const sortedByCallers = [...clusterNodes].sort(
|
|
7424
|
+
(a, b) => (callerCountMap.get(b.id) ?? 0) - (callerCountMap.get(a.id) ?? 0)
|
|
7425
|
+
);
|
|
7426
|
+
const keySymbols = sortedByCallers.slice(0, 5).map((n) => ({
|
|
7427
|
+
name: n.name,
|
|
7428
|
+
callerCount: callerCountMap.get(n.id) ?? 0
|
|
7429
|
+
}));
|
|
7430
|
+
const depsSet = /* @__PURE__ */ new Set();
|
|
7431
|
+
for (const node of clusterNodes) {
|
|
7432
|
+
for (const edge of graph.findEdgesFrom(node.id)) {
|
|
7433
|
+
if (edge.kind !== "imports") continue;
|
|
7434
|
+
const targetNode = graph.getNode(edge.target);
|
|
7435
|
+
if (!targetNode) continue;
|
|
7436
|
+
if (!clusterNodeIds.has(targetNode.id)) {
|
|
7437
|
+
const prefix = getPathPrefix(targetNode.filePath);
|
|
7438
|
+
depsSet.add(prefix);
|
|
7439
|
+
}
|
|
7440
|
+
}
|
|
7441
|
+
}
|
|
7442
|
+
const dependencies = [...depsSet];
|
|
7443
|
+
const dependentsSet = /* @__PURE__ */ new Set();
|
|
7444
|
+
for (const node of clusterNodes) {
|
|
7445
|
+
for (const edge of graph.findEdgesTo(node.id)) {
|
|
7446
|
+
if (edge.kind !== "imports") continue;
|
|
7447
|
+
const sourceNode = graph.getNode(edge.source);
|
|
7448
|
+
if (!sourceNode) continue;
|
|
7449
|
+
if (!clusterNodeIds.has(sourceNode.id)) {
|
|
7450
|
+
const prefix = getPathPrefix(sourceNode.filePath);
|
|
7451
|
+
dependentsSet.add(prefix);
|
|
7452
|
+
}
|
|
7453
|
+
}
|
|
7454
|
+
}
|
|
7455
|
+
const dependents = [...dependentsSet];
|
|
7456
|
+
const healthResult = computeHealthReport(graph, cluster);
|
|
7457
|
+
const health = { score: healthResult.healthScore };
|
|
7458
|
+
const symbolCount = {};
|
|
7459
|
+
for (const node of clusterNodes) {
|
|
7460
|
+
symbolCount[node.kind] = (symbolCount[node.kind] ?? 0) + 1;
|
|
7461
|
+
}
|
|
7462
|
+
let purpose;
|
|
7463
|
+
const topNode = sortedByCallers[0];
|
|
7464
|
+
if (topNode?.metadata?.["summary"] && typeof topNode.metadata["summary"] === "string") {
|
|
7465
|
+
purpose = topNode.metadata["summary"];
|
|
7466
|
+
} else {
|
|
7467
|
+
const clusterName = cluster.split("/").pop() ?? cluster;
|
|
7468
|
+
purpose = `Handles ${clusterName.replace(/[-_/]/g, " ")} functionality`;
|
|
7469
|
+
}
|
|
7470
|
+
return {
|
|
7471
|
+
cluster,
|
|
7472
|
+
purpose,
|
|
7473
|
+
keySymbols,
|
|
7474
|
+
dependencies,
|
|
7475
|
+
dependents,
|
|
7476
|
+
health,
|
|
7477
|
+
symbolCount
|
|
7478
|
+
};
|
|
7479
|
+
}
|
|
7480
|
+
|
|
7481
|
+
// src/mcp-server/server.ts
|
|
7482
|
+
function createMcpServer(graph, repoName, workspaceRoot) {
|
|
7483
|
+
const server = new Server(
|
|
7484
|
+
{ name: "code-intel", version: "0.1.0" },
|
|
7485
|
+
{ capabilities: { tools: {}, resources: {} } }
|
|
7486
|
+
);
|
|
7487
|
+
const _tokenProp = {
|
|
7488
|
+
_token: { type: "string", description: "Required if CODE_INTEL_TOKEN is configured" }
|
|
7489
|
+
};
|
|
7490
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
7491
|
+
tools: [
|
|
7492
|
+
// ── Core repo tools ──────────────────────────────────────────────────
|
|
7493
|
+
{
|
|
7494
|
+
name: "repos",
|
|
7495
|
+
description: "List all indexed repositories with node and edge counts",
|
|
7496
|
+
inputSchema: { type: "object", properties: { ..._tokenProp } }
|
|
7497
|
+
},
|
|
7498
|
+
{
|
|
7499
|
+
name: "overview",
|
|
7500
|
+
description: "Repository summary: total nodes/edges and a full breakdown of node and edge counts by kind. Use this first to understand the shape of the codebase.",
|
|
7501
|
+
inputSchema: { type: "object", properties: { ..._tokenProp } }
|
|
7502
|
+
},
|
|
7503
|
+
// ── Search & inspect ─────────────────────────────────────────────────
|
|
7504
|
+
{
|
|
7505
|
+
name: "search",
|
|
7506
|
+
description: "BM25 keyword search across all indexed symbols \u2014 functions, classes, files, routes, etc.",
|
|
7507
|
+
inputSchema: {
|
|
7508
|
+
type: "object",
|
|
7509
|
+
properties: {
|
|
7510
|
+
query: { type: "string", description: "Search query (symbol name, keyword, or partial match)" },
|
|
7511
|
+
offset: { type: "number", description: "Number of results to skip for pagination (default: 0)" },
|
|
7512
|
+
limit: { type: "number", description: "Max results per page (default: 50, max: 500)" },
|
|
7513
|
+
..._tokenProp
|
|
7514
|
+
},
|
|
7515
|
+
required: ["query"]
|
|
7516
|
+
}
|
|
7517
|
+
},
|
|
7518
|
+
{
|
|
7519
|
+
name: "inspect",
|
|
7520
|
+
description: "360\xB0 view of a symbol: definition location, callers, callees, heritage (extends/implements), members, cluster, and source preview (first 500 chars)",
|
|
7521
|
+
inputSchema: {
|
|
7522
|
+
type: "object",
|
|
7523
|
+
properties: {
|
|
7524
|
+
symbol_name: { type: "string", description: "Exact symbol name to inspect" },
|
|
7525
|
+
..._tokenProp
|
|
7526
|
+
},
|
|
7527
|
+
required: ["symbol_name"]
|
|
7528
|
+
}
|
|
7529
|
+
},
|
|
7530
|
+
{
|
|
7531
|
+
name: "blast_radius",
|
|
7532
|
+
description: "Impact analysis: traverse the call/import graph to find all symbols that depend on or are affected by a given symbol. Returns risk level (LOW / MEDIUM / HIGH).",
|
|
7533
|
+
inputSchema: {
|
|
7534
|
+
type: "object",
|
|
7535
|
+
properties: {
|
|
7536
|
+
target: { type: "string", description: "Target symbol name" },
|
|
7537
|
+
direction: {
|
|
7538
|
+
type: "string",
|
|
7539
|
+
enum: ["callers", "callees", "both"],
|
|
7540
|
+
description: "Which direction to trace \u2014 callers (who depends on it), callees (what it depends on), or both (default: both)"
|
|
7541
|
+
},
|
|
6174
7542
|
max_hops: { type: "number", description: "Maximum traversal depth (default: 5)" },
|
|
6175
7543
|
..._tokenProp
|
|
6176
7544
|
},
|
|
@@ -6184,6 +7552,8 @@ function createMcpServer(graph, repoName, workspaceRoot) {
|
|
|
6184
7552
|
type: "object",
|
|
6185
7553
|
properties: {
|
|
6186
7554
|
file_path: { type: "string", description: 'File path (partial match is supported, e.g. "auth/login.ts")' },
|
|
7555
|
+
offset: { type: "number", description: "Number of results to skip for pagination (default: 0)" },
|
|
7556
|
+
limit: { type: "number", description: "Max results per page (default: 50, max: 500)" },
|
|
6187
7557
|
..._tokenProp
|
|
6188
7558
|
},
|
|
6189
7559
|
required: ["file_path"]
|
|
@@ -6213,7 +7583,8 @@ function createMcpServer(graph, repoName, workspaceRoot) {
|
|
|
6213
7583
|
type: "string",
|
|
6214
7584
|
description: "Filter by node kind: function | class | interface | method | type_alias | constant | enum (optional)"
|
|
6215
7585
|
},
|
|
6216
|
-
|
|
7586
|
+
offset: { type: "number", description: "Number of results to skip for pagination (default: 0)" },
|
|
7587
|
+
limit: { type: "number", description: "Max results per page (default: 50, max: 500)" },
|
|
6217
7588
|
..._tokenProp
|
|
6218
7589
|
}
|
|
6219
7590
|
}
|
|
@@ -6230,7 +7601,8 @@ function createMcpServer(graph, repoName, workspaceRoot) {
|
|
|
6230
7601
|
inputSchema: {
|
|
6231
7602
|
type: "object",
|
|
6232
7603
|
properties: {
|
|
6233
|
-
|
|
7604
|
+
offset: { type: "number", description: "Number of results to skip for pagination (default: 0)" },
|
|
7605
|
+
limit: { type: "number", description: "Max clusters per page (default: 50, max: 500)" },
|
|
6234
7606
|
..._tokenProp
|
|
6235
7607
|
}
|
|
6236
7608
|
}
|
|
@@ -6241,7 +7613,8 @@ function createMcpServer(graph, repoName, workspaceRoot) {
|
|
|
6241
7613
|
inputSchema: {
|
|
6242
7614
|
type: "object",
|
|
6243
7615
|
properties: {
|
|
6244
|
-
|
|
7616
|
+
offset: { type: "number", description: "Number of results to skip for pagination (default: 0)" },
|
|
7617
|
+
limit: { type: "number", description: "Max flows per page (default: 50, max: 500)" },
|
|
6245
7618
|
..._tokenProp
|
|
6246
7619
|
}
|
|
6247
7620
|
}
|
|
@@ -6265,6 +7638,23 @@ function createMcpServer(graph, repoName, workspaceRoot) {
|
|
|
6265
7638
|
}
|
|
6266
7639
|
}
|
|
6267
7640
|
},
|
|
7641
|
+
// ── query (GQL) ────────────────────────────────────────────────────────
|
|
7642
|
+
{
|
|
7643
|
+
name: "query",
|
|
7644
|
+
description: "Execute a GQL (Graph Query Language) query. Supports FIND, TRAVERSE, PATH, and COUNT. More expressive than raw_query.",
|
|
7645
|
+
inputSchema: {
|
|
7646
|
+
type: "object",
|
|
7647
|
+
properties: {
|
|
7648
|
+
gql: {
|
|
7649
|
+
type: "string",
|
|
7650
|
+
description: 'GQL query string. Examples: "FIND function WHERE name CONTAINS \\"auth\\"", "TRAVERSE CALLS FROM \\"handleLogin\\" DEPTH 3", "PATH FROM \\"createUser\\" TO \\"sendEmail\\"", "COUNT function GROUP BY cluster"'
|
|
7651
|
+
},
|
|
7652
|
+
limit: { type: "number", description: "Override LIMIT in the query (optional)" },
|
|
7653
|
+
..._tokenProp
|
|
7654
|
+
},
|
|
7655
|
+
required: ["gql"]
|
|
7656
|
+
}
|
|
7657
|
+
},
|
|
6268
7658
|
// ── Raw query ─────────────────────────────────────────────────────────
|
|
6269
7659
|
{
|
|
6270
7660
|
name: "raw_query",
|
|
@@ -6346,6 +7736,91 @@ function createMcpServer(graph, repoName, workspaceRoot) {
|
|
|
6346
7736
|
},
|
|
6347
7737
|
required: ["name"]
|
|
6348
7738
|
}
|
|
7739
|
+
},
|
|
7740
|
+
// ── Reasoning / analysis tools ────────────────────────────────────────
|
|
7741
|
+
{
|
|
7742
|
+
name: "explain_relationship",
|
|
7743
|
+
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.",
|
|
7744
|
+
inputSchema: {
|
|
7745
|
+
type: "object",
|
|
7746
|
+
properties: {
|
|
7747
|
+
from: { type: "string", description: "Source symbol name" },
|
|
7748
|
+
to: { type: "string", description: "Target symbol name" },
|
|
7749
|
+
..._tokenProp
|
|
7750
|
+
},
|
|
7751
|
+
required: ["from", "to"]
|
|
7752
|
+
}
|
|
7753
|
+
},
|
|
7754
|
+
{
|
|
7755
|
+
name: "pr_impact",
|
|
7756
|
+
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.",
|
|
7757
|
+
inputSchema: {
|
|
7758
|
+
type: "object",
|
|
7759
|
+
properties: {
|
|
7760
|
+
changedFiles: {
|
|
7761
|
+
type: "array",
|
|
7762
|
+
items: { type: "string" },
|
|
7763
|
+
description: "List of changed file paths (relative or absolute)"
|
|
7764
|
+
},
|
|
7765
|
+
diff: {
|
|
7766
|
+
type: "string",
|
|
7767
|
+
description: "Raw unified diff text. Changed files are extracted automatically."
|
|
7768
|
+
},
|
|
7769
|
+
maxHops: {
|
|
7770
|
+
type: "number",
|
|
7771
|
+
description: "Maximum BFS depth for blast radius (default: 5)"
|
|
7772
|
+
},
|
|
7773
|
+
..._tokenProp
|
|
7774
|
+
}
|
|
7775
|
+
}
|
|
7776
|
+
},
|
|
7777
|
+
{
|
|
7778
|
+
name: "similar_symbols",
|
|
7779
|
+
description: "Find symbols with similar names or structure using Levenshtein distance and kind matching. Useful for finding related functions, classes, or interfaces.",
|
|
7780
|
+
inputSchema: {
|
|
7781
|
+
type: "object",
|
|
7782
|
+
properties: {
|
|
7783
|
+
symbol: { type: "string", description: "Symbol name to find similar symbols for" },
|
|
7784
|
+
limit: { type: "number", description: "Maximum number of results (default: 10, max: 50)" },
|
|
7785
|
+
..._tokenProp
|
|
7786
|
+
},
|
|
7787
|
+
required: ["symbol"]
|
|
7788
|
+
}
|
|
7789
|
+
},
|
|
7790
|
+
{
|
|
7791
|
+
name: "health_report",
|
|
7792
|
+
description: "Code health signals for a scope: dead code, cycles, god nodes, orphan files, complexity hotspots",
|
|
7793
|
+
inputSchema: {
|
|
7794
|
+
type: "object",
|
|
7795
|
+
properties: {
|
|
7796
|
+
scope: { type: "string", description: "Directory scope, e.g. 'src/api/' or '.' for whole repo" },
|
|
7797
|
+
..._tokenProp
|
|
7798
|
+
}
|
|
7799
|
+
}
|
|
7800
|
+
},
|
|
7801
|
+
{
|
|
7802
|
+
name: "suggest_tests",
|
|
7803
|
+
description: "Suggest test cases for a symbol: call paths, suggested cases, existing tests, untested callers",
|
|
7804
|
+
inputSchema: {
|
|
7805
|
+
type: "object",
|
|
7806
|
+
properties: {
|
|
7807
|
+
symbol: { type: "string", description: "Symbol name to generate test suggestions for" },
|
|
7808
|
+
..._tokenProp
|
|
7809
|
+
},
|
|
7810
|
+
required: ["symbol"]
|
|
7811
|
+
}
|
|
7812
|
+
},
|
|
7813
|
+
{
|
|
7814
|
+
name: "cluster_summary",
|
|
7815
|
+
description: "Rich summary of a module/cluster: purpose, key symbols, dependencies, health",
|
|
7816
|
+
inputSchema: {
|
|
7817
|
+
type: "object",
|
|
7818
|
+
properties: {
|
|
7819
|
+
cluster: { type: "string", description: "Cluster path e.g. 'src/auth'" },
|
|
7820
|
+
..._tokenProp
|
|
7821
|
+
},
|
|
7822
|
+
required: ["cluster"]
|
|
7823
|
+
}
|
|
6349
7824
|
}
|
|
6350
7825
|
]
|
|
6351
7826
|
}));
|
|
@@ -6416,8 +7891,8 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
|
|
|
6416
7891
|
for (const edge of graph.allEdges()) {
|
|
6417
7892
|
edgeCounts[edge.kind] = (edgeCounts[edge.kind] ?? 0) + 1;
|
|
6418
7893
|
}
|
|
6419
|
-
const { computeHealthReport:
|
|
6420
|
-
const healthReport =
|
|
7894
|
+
const { computeHealthReport: computeHealthReport3 } = await Promise.resolve().then(() => (init_health_score(), health_score_exports));
|
|
7895
|
+
const healthReport = computeHealthReport3(graph);
|
|
6421
7896
|
const health = {
|
|
6422
7897
|
score: Math.round(healthReport.score),
|
|
6423
7898
|
grade: healthReport.grade,
|
|
@@ -6442,10 +7917,37 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
|
|
|
6442
7917
|
// ── search ─────────────────────────────────────────────────────────────
|
|
6443
7918
|
case "search": {
|
|
6444
7919
|
const query = a.query;
|
|
6445
|
-
const
|
|
7920
|
+
const offset = a.offset ?? 0;
|
|
7921
|
+
const effectiveLimit = Math.min(a.limit ?? 50, 500);
|
|
6446
7922
|
const vdbPath = workspaceRoot ? getVectorDbPath(workspaceRoot) : void 0;
|
|
6447
|
-
const
|
|
6448
|
-
|
|
7923
|
+
const fetchLimit = Math.min(offset + effectiveLimit, 500);
|
|
7924
|
+
const { results: allResults, searchMode } = await hybridSearch(graph, query, fetchLimit, { vectorDbPath: vdbPath });
|
|
7925
|
+
const total = allResults.length;
|
|
7926
|
+
const results = allResults.slice(offset, offset + effectiveLimit);
|
|
7927
|
+
const hasMore = offset + effectiveLimit < total;
|
|
7928
|
+
const suggestNextTools = [];
|
|
7929
|
+
const suggestEnabled = process.env["CODE_INTEL_SUGGEST_NEXT_TOOLS"] !== "false";
|
|
7930
|
+
if (suggestEnabled && results.length > 0) {
|
|
7931
|
+
const topName = results[0].name;
|
|
7932
|
+
suggestNextTools.push(
|
|
7933
|
+
{ tool: "inspect", reason: "Inspect the top result in detail", input: { symbol: topName } },
|
|
7934
|
+
{ tool: "similar_symbols", reason: "Find symbols similar to the top result", input: { symbol: topName } }
|
|
7935
|
+
);
|
|
7936
|
+
}
|
|
7937
|
+
return {
|
|
7938
|
+
content: [{
|
|
7939
|
+
type: "text",
|
|
7940
|
+
text: JSON.stringify({
|
|
7941
|
+
results,
|
|
7942
|
+
searchMode,
|
|
7943
|
+
total,
|
|
7944
|
+
offset,
|
|
7945
|
+
limit: effectiveLimit,
|
|
7946
|
+
hasMore,
|
|
7947
|
+
...suggestEnabled ? { suggested_next_tools: suggestNextTools } : {}
|
|
7948
|
+
}, null, 2)
|
|
7949
|
+
}]
|
|
7950
|
+
};
|
|
6449
7951
|
}
|
|
6450
7952
|
// ── inspect ────────────────────────────────────────────────────────────
|
|
6451
7953
|
case "inspect": {
|
|
@@ -6454,6 +7956,26 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
|
|
|
6454
7956
|
if (!node) return { content: [{ type: "text", text: `Symbol "${symbolName}" not found. Try search first.` }] };
|
|
6455
7957
|
const incoming = [...graph.findEdgesTo(node.id)];
|
|
6456
7958
|
const outgoing = [...graph.findEdgesFrom(node.id)];
|
|
7959
|
+
const callers = incoming.filter((e) => e.kind === "calls").map((e) => ({
|
|
7960
|
+
id: e.source,
|
|
7961
|
+
name: graph.getNode(e.source)?.name,
|
|
7962
|
+
file: graph.getNode(e.source)?.filePath
|
|
7963
|
+
}));
|
|
7964
|
+
const callees = outgoing.filter((e) => e.kind === "calls").map((e) => ({
|
|
7965
|
+
id: e.target,
|
|
7966
|
+
name: graph.getNode(e.target)?.name,
|
|
7967
|
+
file: graph.getNode(e.target)?.filePath
|
|
7968
|
+
}));
|
|
7969
|
+
const cluster = incoming.filter((e) => e.kind === "belongs_to").map((e) => graph.getNode(e.target)?.name)[0];
|
|
7970
|
+
const suggestEnabled = process.env["CODE_INTEL_SUGGEST_NEXT_TOOLS"] !== "false";
|
|
7971
|
+
const suggestNextTools = [];
|
|
7972
|
+
if (suggestEnabled) {
|
|
7973
|
+
const topCallerName = callers[0]?.name;
|
|
7974
|
+
suggestNextTools.push(
|
|
7975
|
+
...topCallerName ? [{ tool: "explain_relationship", reason: "Explain connection to a related symbol", input: { from: node.name, to: topCallerName } }] : [],
|
|
7976
|
+
...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 } }]
|
|
7977
|
+
);
|
|
7978
|
+
}
|
|
6457
7979
|
return {
|
|
6458
7980
|
content: [{
|
|
6459
7981
|
type: "text",
|
|
@@ -6467,16 +7989,8 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
|
|
|
6467
7989
|
endLine: node.endLine,
|
|
6468
7990
|
exported: node.exported
|
|
6469
7991
|
},
|
|
6470
|
-
callers
|
|
6471
|
-
|
|
6472
|
-
name: graph.getNode(e.source)?.name,
|
|
6473
|
-
file: graph.getNode(e.source)?.filePath
|
|
6474
|
-
})),
|
|
6475
|
-
callees: outgoing.filter((e) => e.kind === "calls").map((e) => ({
|
|
6476
|
-
id: e.target,
|
|
6477
|
-
name: graph.getNode(e.target)?.name,
|
|
6478
|
-
file: graph.getNode(e.target)?.filePath
|
|
6479
|
-
})),
|
|
7992
|
+
callers,
|
|
7993
|
+
callees,
|
|
6480
7994
|
imports: incoming.filter((e) => e.kind === "imports").map((e) => graph.getNode(e.source)?.name),
|
|
6481
7995
|
importedBy: outgoing.filter((e) => e.kind === "imports").map((e) => graph.getNode(e.target)?.name),
|
|
6482
7996
|
extends: outgoing.filter((e) => e.kind === "extends").map((e) => graph.getNode(e.target)?.name),
|
|
@@ -6485,8 +7999,9 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
|
|
|
6485
7999
|
name: graph.getNode(e.target)?.name,
|
|
6486
8000
|
kind: graph.getNode(e.target)?.kind
|
|
6487
8001
|
})),
|
|
6488
|
-
cluster
|
|
6489
|
-
content: node.content?.slice(0, 500)
|
|
8002
|
+
cluster,
|
|
8003
|
+
content: node.content?.slice(0, 500),
|
|
8004
|
+
...suggestEnabled ? { suggested_next_tools: suggestNextTools } : {}
|
|
6490
8005
|
}, null, 2)
|
|
6491
8006
|
}]
|
|
6492
8007
|
};
|
|
@@ -6522,6 +8037,16 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
|
|
|
6522
8037
|
return n ? { id, name: n.name, kind: n.kind, filePath: n.filePath } : { id };
|
|
6523
8038
|
});
|
|
6524
8039
|
const risk = affected.size > 10 ? "HIGH" : affected.size > 5 ? "MEDIUM" : "LOW";
|
|
8040
|
+
const suggestEnabled = process.env["CODE_INTEL_SUGGEST_NEXT_TOOLS"] !== "false";
|
|
8041
|
+
const suggestNextTools = [];
|
|
8042
|
+
if (suggestEnabled) {
|
|
8043
|
+
const highestRiskSymbol = node.name;
|
|
8044
|
+
const firstFilePath = affectedDetails[0]?.filePath ?? "";
|
|
8045
|
+
suggestNextTools.push(
|
|
8046
|
+
{ tool: "suggest_tests", reason: "Generate tests for the highest-risk symbol", input: { symbol: highestRiskSymbol } },
|
|
8047
|
+
{ tool: "pr_impact", reason: "Compute full PR impact for changed files", input: { changedFiles: [firstFilePath] } }
|
|
8048
|
+
);
|
|
8049
|
+
}
|
|
6525
8050
|
return {
|
|
6526
8051
|
content: [{
|
|
6527
8052
|
type: "text",
|
|
@@ -6529,7 +8054,8 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
|
|
|
6529
8054
|
target: node.name,
|
|
6530
8055
|
affectedCount: affected.size,
|
|
6531
8056
|
riskLevel: risk,
|
|
6532
|
-
affected: affectedDetails
|
|
8057
|
+
affected: affectedDetails,
|
|
8058
|
+
...suggestEnabled ? { suggested_next_tools: suggestNextTools } : {}
|
|
6533
8059
|
}, null, 2)
|
|
6534
8060
|
}]
|
|
6535
8061
|
};
|
|
@@ -6537,17 +8063,27 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
|
|
|
6537
8063
|
// ── file_symbols ───────────────────────────────────────────────────────
|
|
6538
8064
|
case "file_symbols": {
|
|
6539
8065
|
const filePath = a.file_path;
|
|
6540
|
-
const
|
|
8066
|
+
const offset = a.offset ?? 0;
|
|
8067
|
+
const effectiveLimit = Math.min(a.limit ?? 50, 500);
|
|
8068
|
+
const allMatches = [];
|
|
6541
8069
|
for (const node of graph.allNodes()) {
|
|
6542
8070
|
if (node.filePath && node.filePath.includes(filePath)) {
|
|
6543
|
-
|
|
8071
|
+
allMatches.push({ kind: node.kind, name: node.name, startLine: node.startLine, exported: node.exported });
|
|
6544
8072
|
}
|
|
6545
8073
|
}
|
|
6546
|
-
if (
|
|
8074
|
+
if (allMatches.length === 0) {
|
|
6547
8075
|
return { content: [{ type: "text", text: `No symbols found for file path matching "${filePath}".` }] };
|
|
6548
8076
|
}
|
|
6549
|
-
|
|
6550
|
-
|
|
8077
|
+
allMatches.sort((a2, b) => (a2.startLine ?? 0) - (b.startLine ?? 0));
|
|
8078
|
+
const total = allMatches.length;
|
|
8079
|
+
const matches = allMatches.slice(offset, offset + effectiveLimit);
|
|
8080
|
+
const hasMore = offset + effectiveLimit < total;
|
|
8081
|
+
return {
|
|
8082
|
+
content: [{
|
|
8083
|
+
type: "text",
|
|
8084
|
+
text: JSON.stringify({ symbols: matches, total, offset, limit: effectiveLimit, hasMore }, null, 2)
|
|
8085
|
+
}]
|
|
8086
|
+
};
|
|
6551
8087
|
}
|
|
6552
8088
|
// ── find_path ──────────────────────────────────────────────────────────
|
|
6553
8089
|
case "find_path": {
|
|
@@ -6593,15 +8129,23 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
|
|
|
6593
8129
|
// ── list_exports ───────────────────────────────────────────────────────
|
|
6594
8130
|
case "list_exports": {
|
|
6595
8131
|
const kindFilter = a.kind;
|
|
6596
|
-
const
|
|
6597
|
-
const
|
|
8132
|
+
const offset = a.offset ?? 0;
|
|
8133
|
+
const effectiveLimit = Math.min(a.limit ?? 50, 500);
|
|
8134
|
+
const allExports = [];
|
|
6598
8135
|
for (const node of graph.allNodes()) {
|
|
6599
8136
|
if (!node.exported) continue;
|
|
6600
8137
|
if (kindFilter && node.kind !== kindFilter) continue;
|
|
6601
|
-
|
|
6602
|
-
if (exports$1.length >= limit) break;
|
|
8138
|
+
allExports.push({ kind: node.kind, name: node.name, filePath: node.filePath, startLine: node.startLine });
|
|
6603
8139
|
}
|
|
6604
|
-
|
|
8140
|
+
const total = allExports.length;
|
|
8141
|
+
const exports$1 = allExports.slice(offset, offset + effectiveLimit);
|
|
8142
|
+
const hasMore = offset + effectiveLimit < total;
|
|
8143
|
+
return {
|
|
8144
|
+
content: [{
|
|
8145
|
+
type: "text",
|
|
8146
|
+
text: JSON.stringify({ exports: exports$1, total, offset, limit: effectiveLimit, hasMore }, null, 2)
|
|
8147
|
+
}]
|
|
8148
|
+
};
|
|
6605
8149
|
}
|
|
6606
8150
|
// ── routes ─────────────────────────────────────────────────────────────
|
|
6607
8151
|
case "routes": {
|
|
@@ -6615,8 +8159,9 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
|
|
|
6615
8159
|
}
|
|
6616
8160
|
// ── clusters ───────────────────────────────────────────────────────────
|
|
6617
8161
|
case "clusters": {
|
|
6618
|
-
const
|
|
6619
|
-
const
|
|
8162
|
+
const offset = a.offset ?? 0;
|
|
8163
|
+
const effectiveLimit = Math.min(a.limit ?? 50, 500);
|
|
8164
|
+
const allClusters = [];
|
|
6620
8165
|
for (const node of graph.allNodes()) {
|
|
6621
8166
|
if (node.kind === "cluster") {
|
|
6622
8167
|
const members = [];
|
|
@@ -6628,35 +8173,50 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
|
|
|
6628
8173
|
}
|
|
6629
8174
|
}
|
|
6630
8175
|
}
|
|
6631
|
-
|
|
8176
|
+
allClusters.push({
|
|
6632
8177
|
id: node.id,
|
|
6633
8178
|
name: node.name,
|
|
6634
8179
|
memberCount: node.metadata?.memberCount ?? members.length,
|
|
6635
8180
|
topSymbols: members.slice(0, 10)
|
|
6636
8181
|
});
|
|
6637
|
-
if (clusters.length >= limit) break;
|
|
6638
8182
|
}
|
|
6639
8183
|
}
|
|
6640
|
-
|
|
8184
|
+
const total = allClusters.length;
|
|
8185
|
+
const clusters = allClusters.slice(offset, offset + effectiveLimit);
|
|
8186
|
+
const hasMore = offset + effectiveLimit < total;
|
|
8187
|
+
return {
|
|
8188
|
+
content: [{
|
|
8189
|
+
type: "text",
|
|
8190
|
+
text: JSON.stringify({ clusters, total, offset, limit: effectiveLimit, hasMore }, null, 2)
|
|
8191
|
+
}]
|
|
8192
|
+
};
|
|
6641
8193
|
}
|
|
6642
8194
|
// ── flows ──────────────────────────────────────────────────────────────
|
|
6643
8195
|
case "flows": {
|
|
6644
|
-
const
|
|
6645
|
-
const
|
|
8196
|
+
const offset = a.offset ?? 0;
|
|
8197
|
+
const effectiveLimit = Math.min(a.limit ?? 50, 500);
|
|
8198
|
+
const allFlows = [];
|
|
6646
8199
|
for (const node of graph.allNodes()) {
|
|
6647
8200
|
if (node.kind === "flow") {
|
|
6648
8201
|
const steps = node.metadata?.steps;
|
|
6649
|
-
|
|
8202
|
+
allFlows.push({
|
|
6650
8203
|
id: node.id,
|
|
6651
8204
|
name: node.name,
|
|
6652
8205
|
entryPoint: node.metadata?.entryPoint,
|
|
6653
8206
|
steps: steps ?? [],
|
|
6654
8207
|
stepCount: Array.isArray(steps) ? steps.length : 0
|
|
6655
8208
|
});
|
|
6656
|
-
if (flows.length >= limit) break;
|
|
6657
8209
|
}
|
|
6658
8210
|
}
|
|
6659
|
-
|
|
8211
|
+
const total = allFlows.length;
|
|
8212
|
+
const flows = allFlows.slice(offset, offset + effectiveLimit);
|
|
8213
|
+
const hasMore = offset + effectiveLimit < total;
|
|
8214
|
+
return {
|
|
8215
|
+
content: [{
|
|
8216
|
+
type: "text",
|
|
8217
|
+
text: JSON.stringify({ flows, total, offset, limit: effectiveLimit, hasMore }, null, 2)
|
|
8218
|
+
}]
|
|
8219
|
+
};
|
|
6660
8220
|
}
|
|
6661
8221
|
// ── detect_changes ─────────────────────────────────────────────────────
|
|
6662
8222
|
case "detect_changes": {
|
|
@@ -6684,7 +8244,7 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
|
|
|
6684
8244
|
for (const { filePath: changedFile, changedLines } of changedFiles) {
|
|
6685
8245
|
for (const node of graph.allNodes()) {
|
|
6686
8246
|
if (!node.filePath) continue;
|
|
6687
|
-
const normNode = node.filePath.replace(repoRoot + "/", "").replace(repoRoot +
|
|
8247
|
+
const normNode = node.filePath.replace(repoRoot + "/", "").replace(repoRoot + path27.sep, "");
|
|
6688
8248
|
const normChanged = changedFile.replace(/^a\/|^b\//, "");
|
|
6689
8249
|
if (!normNode.endsWith(normChanged) && !normChanged.endsWith(normNode)) continue;
|
|
6690
8250
|
if (node.startLine !== void 0 && node.endLine !== void 0) {
|
|
@@ -6733,16 +8293,51 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
|
|
|
6733
8293
|
}]
|
|
6734
8294
|
};
|
|
6735
8295
|
}
|
|
8296
|
+
// ── query (GQL) ───────────────────────────────────────────────────────────
|
|
8297
|
+
case "query": {
|
|
8298
|
+
const gqlInput = a.gql;
|
|
8299
|
+
if (!gqlInput) {
|
|
8300
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: "Missing required parameter: gql" }) }], isError: true };
|
|
8301
|
+
}
|
|
8302
|
+
const { parseGQL: parseGQL2, isGQLParseError: isGQLParseError2 } = await Promise.resolve().then(() => (init_gql_parser(), gql_parser_exports));
|
|
8303
|
+
const { executeGQL: executeGQL2 } = await Promise.resolve().then(() => (init_gql_executor(), gql_executor_exports));
|
|
8304
|
+
const ast = parseGQL2(gqlInput);
|
|
8305
|
+
if (isGQLParseError2(ast)) {
|
|
8306
|
+
return {
|
|
8307
|
+
content: [{ type: "text", text: JSON.stringify({ error: `GQL parse error: ${ast.message}`, pos: ast.pos, expected: ast.expected, got: ast.got }) }],
|
|
8308
|
+
isError: true
|
|
8309
|
+
};
|
|
8310
|
+
}
|
|
8311
|
+
if (a.limit !== void 0 && ast.type === "FIND") {
|
|
8312
|
+
ast.limit = a.limit;
|
|
8313
|
+
}
|
|
8314
|
+
const result = executeGQL2(ast, graph);
|
|
8315
|
+
return {
|
|
8316
|
+
content: [{
|
|
8317
|
+
type: "text",
|
|
8318
|
+
text: JSON.stringify({
|
|
8319
|
+
nodes: result.nodes,
|
|
8320
|
+
edges: result.edges,
|
|
8321
|
+
groups: result.groups,
|
|
8322
|
+
path: result.path,
|
|
8323
|
+
executionTimeMs: result.executionTimeMs,
|
|
8324
|
+
truncated: result.truncated,
|
|
8325
|
+
totalCount: result.totalCount
|
|
8326
|
+
}, null, 2)
|
|
8327
|
+
}]
|
|
8328
|
+
};
|
|
8329
|
+
}
|
|
6736
8330
|
// ── raw_query ──────────────────────────────────────────────────────────
|
|
6737
8331
|
case "raw_query": {
|
|
6738
8332
|
const q = a.cypher;
|
|
8333
|
+
const deprecationWarning = "raw_query is deprecated, use query instead";
|
|
6739
8334
|
const nameMatch = q?.match(/name\s*=\s*['"]([^'"]+)['"]/i);
|
|
6740
8335
|
if (nameMatch) {
|
|
6741
8336
|
const results = [];
|
|
6742
8337
|
for (const node of graph.allNodes()) {
|
|
6743
8338
|
if (node.name === nameMatch[1]) results.push(node);
|
|
6744
8339
|
}
|
|
6745
|
-
return { content: [{ type: "text", text: JSON.stringify(results, null, 2) }] };
|
|
8340
|
+
return { content: [{ type: "text", text: JSON.stringify({ deprecation: deprecationWarning, results }, null, 2) }] };
|
|
6746
8341
|
}
|
|
6747
8342
|
const kindMatch = q?.match(/:\s*(\w+)/);
|
|
6748
8343
|
if (kindMatch) {
|
|
@@ -6751,9 +8346,9 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
|
|
|
6751
8346
|
if (node.kind === kindMatch[1]) results.push(node);
|
|
6752
8347
|
if (results.length >= 50) break;
|
|
6753
8348
|
}
|
|
6754
|
-
return { content: [{ type: "text", text: JSON.stringify(results, null, 2) }] };
|
|
8349
|
+
return { content: [{ type: "text", text: JSON.stringify({ deprecation: deprecationWarning, results }, null, 2) }] };
|
|
6755
8350
|
}
|
|
6756
|
-
return { content: [{ type: "text", text: "Query not recognized. Use name='X' or :kind syntax." }] };
|
|
8351
|
+
return { content: [{ type: "text", text: JSON.stringify({ deprecation: deprecationWarning, error: "Query not recognized. Use name='X' or :kind syntax. Or use the query tool with GQL instead." }) }] };
|
|
6757
8352
|
}
|
|
6758
8353
|
// ── group_list ─────────────────────────────────────────────────────────
|
|
6759
8354
|
case "group_list": {
|
|
@@ -6871,6 +8466,57 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
|
|
|
6871
8466
|
}]
|
|
6872
8467
|
};
|
|
6873
8468
|
}
|
|
8469
|
+
// ── explain_relationship ───────────────────────────────────────────────
|
|
8470
|
+
case "explain_relationship": {
|
|
8471
|
+
const fromName = a.from;
|
|
8472
|
+
const toName = a.to;
|
|
8473
|
+
const result = explainRelationship(graph, fromName, toName);
|
|
8474
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
8475
|
+
}
|
|
8476
|
+
// ── pr_impact ──────────────────────────────────────────────────────────
|
|
8477
|
+
case "pr_impact": {
|
|
8478
|
+
const maxHops = a.maxHops ?? 5;
|
|
8479
|
+
let changedFiles = a.changedFiles ?? [];
|
|
8480
|
+
if (a.diff && typeof a.diff === "string") {
|
|
8481
|
+
const diffFiles = parseDiffFiles(a.diff);
|
|
8482
|
+
changedFiles = [.../* @__PURE__ */ new Set([...changedFiles, ...diffFiles])];
|
|
8483
|
+
}
|
|
8484
|
+
if (changedFiles.length === 0) {
|
|
8485
|
+
return {
|
|
8486
|
+
content: [{
|
|
8487
|
+
type: "text",
|
|
8488
|
+
text: JSON.stringify({ error: 'No changed files provided. Supply "changedFiles" or "diff".' })
|
|
8489
|
+
}]
|
|
8490
|
+
};
|
|
8491
|
+
}
|
|
8492
|
+
const result = computePRImpact(graph, changedFiles, maxHops);
|
|
8493
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
8494
|
+
}
|
|
8495
|
+
// ── similar_symbols ────────────────────────────────────────────────────
|
|
8496
|
+
case "similar_symbols": {
|
|
8497
|
+
const symbolName = a.symbol;
|
|
8498
|
+
const limit = a.limit ?? 10;
|
|
8499
|
+
const result = findSimilarSymbols(graph, symbolName, limit);
|
|
8500
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
8501
|
+
}
|
|
8502
|
+
// ── health_report ──────────────────────────────────────────────────────
|
|
8503
|
+
case "health_report": {
|
|
8504
|
+
const scope = a.scope ?? ".";
|
|
8505
|
+
const result = computeHealthReport(graph, scope);
|
|
8506
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
8507
|
+
}
|
|
8508
|
+
// ── suggest_tests ──────────────────────────────────────────────────────
|
|
8509
|
+
case "suggest_tests": {
|
|
8510
|
+
const sym = a.symbol;
|
|
8511
|
+
const result = suggestTests(graph, sym);
|
|
8512
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
8513
|
+
}
|
|
8514
|
+
// ── cluster_summary ────────────────────────────────────────────────────
|
|
8515
|
+
case "cluster_summary": {
|
|
8516
|
+
const cluster = a.cluster;
|
|
8517
|
+
const result = summarizeCluster(graph, cluster);
|
|
8518
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
8519
|
+
}
|
|
6874
8520
|
default:
|
|
6875
8521
|
return { content: [{ type: "text", text: `Unknown tool: ${name}` }] };
|
|
6876
8522
|
}
|
|
@@ -6964,7 +8610,7 @@ var STUCK_THRESHOLD_MINUTES = 30;
|
|
|
6964
8610
|
var JobsDB = class {
|
|
6965
8611
|
db;
|
|
6966
8612
|
constructor(dbPath) {
|
|
6967
|
-
fs19.mkdirSync(
|
|
8613
|
+
fs19.mkdirSync(path27.dirname(dbPath), { recursive: true });
|
|
6968
8614
|
this.db = new Database3(dbPath);
|
|
6969
8615
|
this.db.pragma("journal_mode = WAL");
|
|
6970
8616
|
this.db.pragma("foreign_keys = ON");
|
|
@@ -7106,7 +8752,7 @@ var JobsDB = class {
|
|
|
7106
8752
|
}
|
|
7107
8753
|
};
|
|
7108
8754
|
function getJobsDBPath() {
|
|
7109
|
-
return
|
|
8755
|
+
return path27.join(os12.homedir(), ".code-intel", "jobs.db");
|
|
7110
8756
|
}
|
|
7111
8757
|
var _jobsDB = null;
|
|
7112
8758
|
function getOrCreateJobsDB() {
|
|
@@ -7198,7 +8844,7 @@ var BACKUP_VERSION = "1.0";
|
|
|
7198
8844
|
var ALGORITHM = "aes-256-gcm";
|
|
7199
8845
|
var IV_LENGTH = 16;
|
|
7200
8846
|
function getBackupDir() {
|
|
7201
|
-
return
|
|
8847
|
+
return path27.join(os12.homedir(), ".code-intel", "backups");
|
|
7202
8848
|
}
|
|
7203
8849
|
function getBackupKey() {
|
|
7204
8850
|
const keyHex = process.env["CODE_INTEL_BACKUP_KEY"];
|
|
@@ -7236,22 +8882,22 @@ var BackupService = class {
|
|
|
7236
8882
|
* Returns the backup entry.
|
|
7237
8883
|
*/
|
|
7238
8884
|
createBackup(repoPath) {
|
|
7239
|
-
const codeIntelDir =
|
|
8885
|
+
const codeIntelDir = path27.join(repoPath, ".code-intel");
|
|
7240
8886
|
const id = v4();
|
|
7241
8887
|
const createdAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
7242
8888
|
const filesToBackup = [];
|
|
7243
8889
|
const candidates = ["graph.db", "vector.db", "meta.json"];
|
|
7244
8890
|
for (const f of candidates) {
|
|
7245
|
-
const fp =
|
|
8891
|
+
const fp = path27.join(codeIntelDir, f);
|
|
7246
8892
|
if (fs19.existsSync(fp)) {
|
|
7247
8893
|
filesToBackup.push({ name: f, localPath: fp });
|
|
7248
8894
|
}
|
|
7249
8895
|
}
|
|
7250
|
-
const registryPath =
|
|
8896
|
+
const registryPath = path27.join(os12.homedir(), ".code-intel", "registry.json");
|
|
7251
8897
|
if (fs19.existsSync(registryPath)) {
|
|
7252
8898
|
filesToBackup.push({ name: "registry.json", localPath: registryPath });
|
|
7253
8899
|
}
|
|
7254
|
-
const usersDbPath =
|
|
8900
|
+
const usersDbPath = path27.join(os12.homedir(), ".code-intel", "users.db");
|
|
7255
8901
|
if (fs19.existsSync(usersDbPath)) {
|
|
7256
8902
|
filesToBackup.push({ name: "users.db", localPath: usersDbPath });
|
|
7257
8903
|
}
|
|
@@ -7288,7 +8934,7 @@ var BackupService = class {
|
|
|
7288
8934
|
const plaintext = Buffer.concat(parts);
|
|
7289
8935
|
const encrypted = encryptBuffer(plaintext, this.key);
|
|
7290
8936
|
const backupFileName = `backup-${id}.cib`;
|
|
7291
|
-
const backupPath =
|
|
8937
|
+
const backupPath = path27.join(this.backupDir, backupFileName);
|
|
7292
8938
|
fs19.writeFileSync(backupPath, encrypted);
|
|
7293
8939
|
const entry = {
|
|
7294
8940
|
id,
|
|
@@ -7316,7 +8962,7 @@ var BackupService = class {
|
|
|
7316
8962
|
async uploadToS3(entry) {
|
|
7317
8963
|
const cfg = getS3Config();
|
|
7318
8964
|
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.");
|
|
7319
|
-
const fileName =
|
|
8965
|
+
const fileName = path27.basename(entry.path);
|
|
7320
8966
|
const s3Key = `${cfg.prefix}${fileName}`;
|
|
7321
8967
|
const body = fs19.readFileSync(entry.path);
|
|
7322
8968
|
const result = await s3Request({ method: "PUT", cfg, key: s3Key, body });
|
|
@@ -7335,7 +8981,7 @@ var BackupService = class {
|
|
|
7335
8981
|
if (result.statusCode < 200 || result.statusCode >= 300) {
|
|
7336
8982
|
throw new Error(`S3 download failed (HTTP ${result.statusCode}): ${result.body.slice(0, 200)}`);
|
|
7337
8983
|
}
|
|
7338
|
-
fs19.mkdirSync(
|
|
8984
|
+
fs19.mkdirSync(path27.dirname(destPath), { recursive: true });
|
|
7339
8985
|
fs19.writeFileSync(destPath, Buffer.from(result.body, "binary"));
|
|
7340
8986
|
}
|
|
7341
8987
|
/**
|
|
@@ -7399,7 +9045,7 @@ var BackupService = class {
|
|
|
7399
9045
|
offset += manifestLen;
|
|
7400
9046
|
const manifest = JSON.parse(manifestStr);
|
|
7401
9047
|
const restoreBase = targetRepoPath ?? entry.repoPath;
|
|
7402
|
-
const codeIntelDir =
|
|
9048
|
+
const codeIntelDir = path27.join(restoreBase, ".code-intel");
|
|
7403
9049
|
fs19.mkdirSync(codeIntelDir, { recursive: true });
|
|
7404
9050
|
for (const fileEntry of manifest.files) {
|
|
7405
9051
|
const nameLen = plaintext.readUInt16BE(offset);
|
|
@@ -7417,9 +9063,9 @@ var BackupService = class {
|
|
|
7417
9063
|
}
|
|
7418
9064
|
let destPath;
|
|
7419
9065
|
if (name === "registry.json" || name === "users.db") {
|
|
7420
|
-
destPath =
|
|
9066
|
+
destPath = path27.join(os12.homedir(), ".code-intel", name);
|
|
7421
9067
|
} else {
|
|
7422
|
-
destPath =
|
|
9068
|
+
destPath = path27.join(codeIntelDir, name);
|
|
7423
9069
|
}
|
|
7424
9070
|
fs19.writeFileSync(destPath, data);
|
|
7425
9071
|
}
|
|
@@ -7470,7 +9116,7 @@ var BackupService = class {
|
|
|
7470
9116
|
}
|
|
7471
9117
|
// ── Index helpers ──────────────────────────────────────────────────────────
|
|
7472
9118
|
_indexPath() {
|
|
7473
|
-
return
|
|
9119
|
+
return path27.join(this.backupDir, "index.json");
|
|
7474
9120
|
}
|
|
7475
9121
|
_loadIndex() {
|
|
7476
9122
|
try {
|
|
@@ -7971,6 +9617,60 @@ var openApiSpec = {
|
|
|
7971
9617
|
}
|
|
7972
9618
|
}
|
|
7973
9619
|
},
|
|
9620
|
+
"/source": {
|
|
9621
|
+
get: {
|
|
9622
|
+
tags: ["Files"],
|
|
9623
|
+
summary: "Get source code preview with context around specified lines",
|
|
9624
|
+
description: "Returns the file content around the specified line range (\xB120 lines context), with language detection. Requires viewer role.",
|
|
9625
|
+
security: [{ BearerAuth: [] }, { SessionCookie: [] }],
|
|
9626
|
+
parameters: [
|
|
9627
|
+
{
|
|
9628
|
+
name: "file",
|
|
9629
|
+
in: "query",
|
|
9630
|
+
required: true,
|
|
9631
|
+
description: "Absolute path to the file",
|
|
9632
|
+
schema: { type: "string" }
|
|
9633
|
+
},
|
|
9634
|
+
{
|
|
9635
|
+
name: "startLine",
|
|
9636
|
+
in: "query",
|
|
9637
|
+
required: false,
|
|
9638
|
+
description: "Start line number (1-indexed)",
|
|
9639
|
+
schema: { type: "integer", minimum: 1 }
|
|
9640
|
+
},
|
|
9641
|
+
{
|
|
9642
|
+
name: "endLine",
|
|
9643
|
+
in: "query",
|
|
9644
|
+
required: false,
|
|
9645
|
+
description: "End line number (1-indexed)",
|
|
9646
|
+
schema: { type: "integer", minimum: 1 }
|
|
9647
|
+
}
|
|
9648
|
+
],
|
|
9649
|
+
responses: {
|
|
9650
|
+
"200": {
|
|
9651
|
+
description: "Source code preview",
|
|
9652
|
+
content: {
|
|
9653
|
+
"application/json": {
|
|
9654
|
+
schema: {
|
|
9655
|
+
type: "object",
|
|
9656
|
+
properties: {
|
|
9657
|
+
content: { type: "string", description: "File content (with context lines)" },
|
|
9658
|
+
language: { type: "string", description: "Detected programming language", example: "typescript" },
|
|
9659
|
+
startLine: { type: "integer", description: "Actual start line returned (with context)" },
|
|
9660
|
+
endLine: { type: "integer", description: "Actual end line returned (with context)" }
|
|
9661
|
+
},
|
|
9662
|
+
required: ["content", "language", "startLine", "endLine"]
|
|
9663
|
+
}
|
|
9664
|
+
}
|
|
9665
|
+
}
|
|
9666
|
+
},
|
|
9667
|
+
"400": { description: "Bad request (missing file param or path traversal detected)", content: { "application/json": { schema: { "$ref": "#/components/schemas/ErrorResponse" } } } },
|
|
9668
|
+
"401": { description: "Unauthorized", content: { "application/json": { schema: { "$ref": "#/components/schemas/ErrorResponse" } } } },
|
|
9669
|
+
"403": { description: "Forbidden (file outside indexed repos)", content: { "application/json": { schema: { "$ref": "#/components/schemas/ErrorResponse" } } } },
|
|
9670
|
+
"404": { description: "File not found", content: { "application/json": { schema: { "$ref": "#/components/schemas/ErrorResponse" } } } }
|
|
9671
|
+
}
|
|
9672
|
+
}
|
|
9673
|
+
},
|
|
7974
9674
|
"/grep": {
|
|
7975
9675
|
post: {
|
|
7976
9676
|
tags: ["Files"],
|
|
@@ -8084,16 +9784,122 @@ var openApiSpec = {
|
|
|
8084
9784
|
"404": { description: "Group not found", content: { "application/json": { schema: { "$ref": "#/components/schemas/ErrorResponse" } } } }
|
|
8085
9785
|
}
|
|
8086
9786
|
}
|
|
9787
|
+
},
|
|
9788
|
+
"/query": {
|
|
9789
|
+
post: {
|
|
9790
|
+
tags: ["GQL"],
|
|
9791
|
+
summary: "Execute a GQL (Graph Query Language) query against the knowledge graph",
|
|
9792
|
+
description: "Supports FIND, TRAVERSE, PATH, and COUNT statements. Requires viewer role minimum.",
|
|
9793
|
+
security: [{ BearerAuth: [] }, { SessionCookie: [] }],
|
|
9794
|
+
requestBody: {
|
|
9795
|
+
required: true,
|
|
9796
|
+
content: {
|
|
9797
|
+
"application/json": {
|
|
9798
|
+
schema: {
|
|
9799
|
+
type: "object",
|
|
9800
|
+
properties: {
|
|
9801
|
+
gql: {
|
|
9802
|
+
type: "string",
|
|
9803
|
+
description: "GQL query string",
|
|
9804
|
+
example: 'FIND function WHERE name CONTAINS "auth"'
|
|
9805
|
+
},
|
|
9806
|
+
format: {
|
|
9807
|
+
type: "string",
|
|
9808
|
+
enum: ["json", "table", "csv"],
|
|
9809
|
+
default: "json",
|
|
9810
|
+
description: "Output format"
|
|
9811
|
+
}
|
|
9812
|
+
},
|
|
9813
|
+
required: ["gql"]
|
|
9814
|
+
}
|
|
9815
|
+
}
|
|
9816
|
+
}
|
|
9817
|
+
},
|
|
9818
|
+
responses: {
|
|
9819
|
+
"200": {
|
|
9820
|
+
description: "GQL execution result",
|
|
9821
|
+
content: {
|
|
9822
|
+
"application/json": {
|
|
9823
|
+
schema: {
|
|
9824
|
+
type: "object",
|
|
9825
|
+
properties: {
|
|
9826
|
+
nodes: { type: "array", items: { "$ref": "#/components/schemas/CodeNode" } },
|
|
9827
|
+
edges: { type: "array", items: { type: "object" } },
|
|
9828
|
+
groups: { type: "array", items: { type: "object", properties: { key: { type: "string" }, count: { type: "integer" } } } },
|
|
9829
|
+
path: { type: "array", items: { "$ref": "#/components/schemas/CodeNode" }, nullable: true },
|
|
9830
|
+
executionTimeMs: { type: "number" },
|
|
9831
|
+
truncated: { type: "boolean" },
|
|
9832
|
+
totalCount: { type: "integer" }
|
|
9833
|
+
}
|
|
9834
|
+
}
|
|
9835
|
+
}
|
|
9836
|
+
}
|
|
9837
|
+
},
|
|
9838
|
+
"400": { description: "Missing gql field", content: { "application/json": { schema: { "$ref": "#/components/schemas/ErrorResponse" } } } },
|
|
9839
|
+
"401": { description: "Unauthorized", content: { "application/json": { schema: { "$ref": "#/components/schemas/ErrorResponse" } } } },
|
|
9840
|
+
"403": { description: "Forbidden (insufficient role)", content: { "application/json": { schema: { "$ref": "#/components/schemas/ErrorResponse" } } } },
|
|
9841
|
+
"422": { description: "GQL parse error", content: { "application/json": { schema: { "$ref": "#/components/schemas/ErrorResponse" } } } }
|
|
9842
|
+
}
|
|
9843
|
+
}
|
|
9844
|
+
},
|
|
9845
|
+
"/query/explain": {
|
|
9846
|
+
post: {
|
|
9847
|
+
tags: ["GQL"],
|
|
9848
|
+
summary: "Explain a GQL query \u2014 returns the execution plan without running it",
|
|
9849
|
+
description: "Returns a query plan object describing the steps that would be executed. Requires viewer role minimum.",
|
|
9850
|
+
security: [{ BearerAuth: [] }, { SessionCookie: [] }],
|
|
9851
|
+
requestBody: {
|
|
9852
|
+
required: true,
|
|
9853
|
+
content: {
|
|
9854
|
+
"application/json": {
|
|
9855
|
+
schema: {
|
|
9856
|
+
type: "object",
|
|
9857
|
+
properties: {
|
|
9858
|
+
gql: { type: "string", description: "GQL query string", example: 'FIND function WHERE name CONTAINS "auth"' }
|
|
9859
|
+
},
|
|
9860
|
+
required: ["gql"]
|
|
9861
|
+
}
|
|
9862
|
+
}
|
|
9863
|
+
}
|
|
9864
|
+
},
|
|
9865
|
+
responses: {
|
|
9866
|
+
"200": {
|
|
9867
|
+
description: "Query plan",
|
|
9868
|
+
content: {
|
|
9869
|
+
"application/json": {
|
|
9870
|
+
schema: {
|
|
9871
|
+
type: "object",
|
|
9872
|
+
properties: {
|
|
9873
|
+
plan: {
|
|
9874
|
+
type: "object",
|
|
9875
|
+
properties: {
|
|
9876
|
+
type: { type: "string", enum: ["FIND", "TRAVERSE", "PATH", "COUNT"] },
|
|
9877
|
+
gql: { type: "string" },
|
|
9878
|
+
steps: { type: "array", items: { type: "object" } },
|
|
9879
|
+
estimatedCost: { type: "number" }
|
|
9880
|
+
}
|
|
9881
|
+
},
|
|
9882
|
+
graphSize: { type: "object", properties: { nodes: { type: "integer" }, edges: { type: "integer" } } }
|
|
9883
|
+
}
|
|
9884
|
+
}
|
|
9885
|
+
}
|
|
9886
|
+
}
|
|
9887
|
+
},
|
|
9888
|
+
"400": { description: "Missing gql field", content: { "application/json": { schema: { "$ref": "#/components/schemas/ErrorResponse" } } } },
|
|
9889
|
+
"401": { description: "Unauthorized", content: { "application/json": { schema: { "$ref": "#/components/schemas/ErrorResponse" } } } },
|
|
9890
|
+
"422": { description: "GQL parse error", content: { "application/json": { schema: { "$ref": "#/components/schemas/ErrorResponse" } } } }
|
|
9891
|
+
}
|
|
9892
|
+
}
|
|
8087
9893
|
}
|
|
8088
9894
|
}
|
|
8089
9895
|
};
|
|
8090
9896
|
|
|
8091
9897
|
// src/http/app.ts
|
|
8092
|
-
var __dirname$1 =
|
|
9898
|
+
var __dirname$1 = path27.dirname(fileURLToPath(import.meta.url));
|
|
8093
9899
|
var WEB_DIST = (() => {
|
|
8094
|
-
const bundled =
|
|
9900
|
+
const bundled = path27.resolve(__dirname$1, "..", "web");
|
|
8095
9901
|
if (fs19.existsSync(bundled)) return bundled;
|
|
8096
|
-
return
|
|
9902
|
+
return path27.resolve(__dirname$1, "..", "..", "..", "web", "dist");
|
|
8097
9903
|
})();
|
|
8098
9904
|
function getAllowedOrigins() {
|
|
8099
9905
|
const env = process.env["CODE_INTEL_CORS_ORIGINS"];
|
|
@@ -8121,6 +9927,7 @@ function createDefaultLimiter() {
|
|
|
8121
9927
|
function createApp(graph, repoName, workspaceRoot, watcherState) {
|
|
8122
9928
|
const app = express();
|
|
8123
9929
|
app.set("trust proxy", 1);
|
|
9930
|
+
app.use(compression());
|
|
8124
9931
|
app.use(
|
|
8125
9932
|
helmet({
|
|
8126
9933
|
contentSecurityPolicy: false
|
|
@@ -8623,7 +10430,7 @@ function createApp(graph, repoName, workspaceRoot, watcherState) {
|
|
|
8623
10430
|
const registry = loadRegistry();
|
|
8624
10431
|
const entry = registry.find((r) => r.name === requestedRepo || r.path === requestedRepo);
|
|
8625
10432
|
if (!entry) return null;
|
|
8626
|
-
const dbPath =
|
|
10433
|
+
const dbPath = path27.join(entry.path, ".code-intel", "graph.db");
|
|
8627
10434
|
if (!fs19.existsSync(dbPath)) return null;
|
|
8628
10435
|
const repoGraph = createKnowledgeGraph();
|
|
8629
10436
|
const db = new DbManager(dbPath);
|
|
@@ -8969,7 +10776,7 @@ function createApp(graph, repoName, workspaceRoot, watcherState) {
|
|
|
8969
10776
|
for (const member of group.members) {
|
|
8970
10777
|
const regEntry = registry.find((r) => r.name === member.registryName);
|
|
8971
10778
|
if (!regEntry) continue;
|
|
8972
|
-
const dbPath =
|
|
10779
|
+
const dbPath = path27.join(regEntry.path, ".code-intel", "graph.db");
|
|
8973
10780
|
if (!fs19.existsSync(dbPath)) continue;
|
|
8974
10781
|
const db = new DbManager(dbPath);
|
|
8975
10782
|
try {
|
|
@@ -8982,10 +10789,245 @@ function createApp(graph, repoName, workspaceRoot, watcherState) {
|
|
|
8982
10789
|
}
|
|
8983
10790
|
res.json({ nodes: [...mergedGraph.allNodes()], edges: [...mergedGraph.allEdges()] });
|
|
8984
10791
|
});
|
|
10792
|
+
app.get("/api/v1/source", requireAuth, requireRole("viewer"), (req, res) => {
|
|
10793
|
+
const { file, startLine: startLineStr, endLine: endLineStr } = req.query;
|
|
10794
|
+
if (!file) {
|
|
10795
|
+
res.status(400).json({
|
|
10796
|
+
error: {
|
|
10797
|
+
code: ErrorCodes.INVALID_REQUEST,
|
|
10798
|
+
message: "Missing required query parameter: file",
|
|
10799
|
+
requestId: req.requestId,
|
|
10800
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
10801
|
+
}
|
|
10802
|
+
});
|
|
10803
|
+
return;
|
|
10804
|
+
}
|
|
10805
|
+
if (file.includes("../")) {
|
|
10806
|
+
res.status(400).json({
|
|
10807
|
+
error: {
|
|
10808
|
+
code: ErrorCodes.INVALID_REQUEST,
|
|
10809
|
+
message: "Path traversal detected",
|
|
10810
|
+
hint: 'File paths must not contain "../"',
|
|
10811
|
+
requestId: req.requestId,
|
|
10812
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
10813
|
+
}
|
|
10814
|
+
});
|
|
10815
|
+
return;
|
|
10816
|
+
}
|
|
10817
|
+
let rawResolved = path27.normalize(file);
|
|
10818
|
+
if (!path27.isAbsolute(rawResolved) && workspaceRoot) {
|
|
10819
|
+
rawResolved = path27.join(workspaceRoot, rawResolved);
|
|
10820
|
+
}
|
|
10821
|
+
const resolvedFile = path27.resolve(rawResolved);
|
|
10822
|
+
function isInsideDir(fileAbs, dir) {
|
|
10823
|
+
const rel = path27.relative(path27.resolve(dir), fileAbs);
|
|
10824
|
+
return !rel.startsWith("..") && !path27.isAbsolute(rel);
|
|
10825
|
+
}
|
|
10826
|
+
if (workspaceRoot) {
|
|
10827
|
+
if (!isInsideDir(resolvedFile, workspaceRoot)) {
|
|
10828
|
+
const registry = loadRegistry();
|
|
10829
|
+
const inKnownRepo = registry.some((r) => isInsideDir(resolvedFile, r.path));
|
|
10830
|
+
if (!inKnownRepo) {
|
|
10831
|
+
res.status(403).json({
|
|
10832
|
+
error: {
|
|
10833
|
+
code: ErrorCodes.FORBIDDEN,
|
|
10834
|
+
message: "Access denied",
|
|
10835
|
+
hint: "File path must be within an indexed repository",
|
|
10836
|
+
requestId: req.requestId,
|
|
10837
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
10838
|
+
}
|
|
10839
|
+
});
|
|
10840
|
+
return;
|
|
10841
|
+
}
|
|
10842
|
+
}
|
|
10843
|
+
} else {
|
|
10844
|
+
const registry = loadRegistry();
|
|
10845
|
+
const inKnownRepo = registry.some((r) => isInsideDir(resolvedFile, r.path));
|
|
10846
|
+
if (!inKnownRepo) {
|
|
10847
|
+
res.status(403).json({
|
|
10848
|
+
error: {
|
|
10849
|
+
code: ErrorCodes.FORBIDDEN,
|
|
10850
|
+
message: "Access denied",
|
|
10851
|
+
hint: "File path must be within an indexed repository",
|
|
10852
|
+
requestId: req.requestId,
|
|
10853
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
10854
|
+
}
|
|
10855
|
+
});
|
|
10856
|
+
return;
|
|
10857
|
+
}
|
|
10858
|
+
}
|
|
10859
|
+
let fileContent;
|
|
10860
|
+
try {
|
|
10861
|
+
fileContent = fs19.readFileSync(resolvedFile, "utf-8");
|
|
10862
|
+
} catch {
|
|
10863
|
+
res.status(404).json({
|
|
10864
|
+
error: {
|
|
10865
|
+
code: ErrorCodes.NOT_FOUND,
|
|
10866
|
+
message: "File not found",
|
|
10867
|
+
requestId: req.requestId,
|
|
10868
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
10869
|
+
}
|
|
10870
|
+
});
|
|
10871
|
+
return;
|
|
10872
|
+
}
|
|
10873
|
+
const lines = fileContent.split("\n");
|
|
10874
|
+
const parsedStart = startLineStr ? Number.parseInt(startLineStr, 10) : 1;
|
|
10875
|
+
const parsedEnd = endLineStr ? Number.parseInt(endLineStr, 10) : parsedStart;
|
|
10876
|
+
if (!Number.isFinite(parsedStart) || parsedStart < 1 || !Number.isFinite(parsedEnd) || parsedEnd < 1) {
|
|
10877
|
+
res.status(400).json({
|
|
10878
|
+
error: {
|
|
10879
|
+
code: ErrorCodes.INVALID_REQUEST,
|
|
10880
|
+
message: "Invalid startLine or endLine: must be positive integers",
|
|
10881
|
+
requestId: req.requestId,
|
|
10882
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
10883
|
+
}
|
|
10884
|
+
});
|
|
10885
|
+
return;
|
|
10886
|
+
}
|
|
10887
|
+
const startLine = Math.max(1, parsedStart);
|
|
10888
|
+
const endLine = Math.min(lines.length, parsedEnd);
|
|
10889
|
+
const contextStart = Math.max(1, startLine - 20);
|
|
10890
|
+
const contextEnd = Math.min(lines.length, endLine + 20);
|
|
10891
|
+
const content = lines.slice(contextStart - 1, contextEnd).join("\n");
|
|
10892
|
+
const ext = path27.extname(resolvedFile).toLowerCase();
|
|
10893
|
+
const languageMap = {
|
|
10894
|
+
".ts": "typescript",
|
|
10895
|
+
".tsx": "typescript",
|
|
10896
|
+
".js": "javascript",
|
|
10897
|
+
".jsx": "javascript",
|
|
10898
|
+
".mjs": "javascript",
|
|
10899
|
+
".cjs": "javascript",
|
|
10900
|
+
".py": "python",
|
|
10901
|
+
".go": "go",
|
|
10902
|
+
".rs": "rust",
|
|
10903
|
+
".java": "java",
|
|
10904
|
+
".cs": "csharp",
|
|
10905
|
+
".cpp": "cpp",
|
|
10906
|
+
".cc": "cpp",
|
|
10907
|
+
".cxx": "cpp",
|
|
10908
|
+
".c": "c",
|
|
10909
|
+
".h": "c",
|
|
10910
|
+
".hpp": "cpp",
|
|
10911
|
+
".rb": "ruby",
|
|
10912
|
+
".php": "php",
|
|
10913
|
+
".swift": "swift",
|
|
10914
|
+
".kt": "kotlin",
|
|
10915
|
+
".kts": "kotlin",
|
|
10916
|
+
".json": "json",
|
|
10917
|
+
".yaml": "yaml",
|
|
10918
|
+
".yml": "yaml",
|
|
10919
|
+
".md": "markdown",
|
|
10920
|
+
".sh": "bash",
|
|
10921
|
+
".bash": "bash",
|
|
10922
|
+
".zsh": "bash",
|
|
10923
|
+
".sql": "sql",
|
|
10924
|
+
".html": "html",
|
|
10925
|
+
".htm": "html",
|
|
10926
|
+
".css": "css",
|
|
10927
|
+
".scss": "scss",
|
|
10928
|
+
".less": "less",
|
|
10929
|
+
".xml": "xml",
|
|
10930
|
+
".toml": "toml"
|
|
10931
|
+
};
|
|
10932
|
+
const language = languageMap[ext] ?? "plaintext";
|
|
10933
|
+
res.json({
|
|
10934
|
+
content,
|
|
10935
|
+
language,
|
|
10936
|
+
startLine: contextStart,
|
|
10937
|
+
endLine: contextEnd
|
|
10938
|
+
});
|
|
10939
|
+
});
|
|
10940
|
+
app.post("/api/v1/query", requireRole("viewer"), async (req, res) => {
|
|
10941
|
+
const { gql, format } = req.body;
|
|
10942
|
+
if (!gql || typeof gql !== "string") {
|
|
10943
|
+
res.status(400).json({
|
|
10944
|
+
error: { code: ErrorCodes.INVALID_REQUEST, message: "Missing required field: gql", requestId: req.requestId, timestamp: (/* @__PURE__ */ new Date()).toISOString() }
|
|
10945
|
+
});
|
|
10946
|
+
return;
|
|
10947
|
+
}
|
|
10948
|
+
try {
|
|
10949
|
+
const { parseGQL: parseGQL2, isGQLParseError: isGQLParseError2 } = await Promise.resolve().then(() => (init_gql_parser(), gql_parser_exports));
|
|
10950
|
+
const { executeGQL: executeGQL2 } = await Promise.resolve().then(() => (init_gql_executor(), gql_executor_exports));
|
|
10951
|
+
const ast = parseGQL2(gql);
|
|
10952
|
+
if (isGQLParseError2(ast)) {
|
|
10953
|
+
res.status(422).json({
|
|
10954
|
+
error: {
|
|
10955
|
+
code: ErrorCodes.INVALID_REQUEST,
|
|
10956
|
+
message: `GQL parse error: ${ast.message}`,
|
|
10957
|
+
hint: `Position: ${ast.pos}${ast.expected ? `, expected: ${ast.expected}` : ""}${ast.got ? `, got: ${ast.got}` : ""}`,
|
|
10958
|
+
requestId: req.requestId,
|
|
10959
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
10960
|
+
}
|
|
10961
|
+
});
|
|
10962
|
+
return;
|
|
10963
|
+
}
|
|
10964
|
+
const result = executeGQL2(ast, graph);
|
|
10965
|
+
const statusCode = result.truncated ? 408 : 200;
|
|
10966
|
+
res.status(statusCode).json({ ...result, format: format ?? "json" });
|
|
10967
|
+
} catch (err) {
|
|
10968
|
+
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() } });
|
|
10969
|
+
}
|
|
10970
|
+
});
|
|
10971
|
+
app.post("/api/v1/query/explain", requireRole("viewer"), async (req, res) => {
|
|
10972
|
+
const { gql } = req.body;
|
|
10973
|
+
if (!gql || typeof gql !== "string") {
|
|
10974
|
+
res.status(400).json({
|
|
10975
|
+
error: { code: ErrorCodes.INVALID_REQUEST, message: "Missing required field: gql", requestId: req.requestId, timestamp: (/* @__PURE__ */ new Date()).toISOString() }
|
|
10976
|
+
});
|
|
10977
|
+
return;
|
|
10978
|
+
}
|
|
10979
|
+
try {
|
|
10980
|
+
const { parseGQL: parseGQL2, isGQLParseError: isGQLParseError2 } = await Promise.resolve().then(() => (init_gql_parser(), gql_parser_exports));
|
|
10981
|
+
const ast = parseGQL2(gql);
|
|
10982
|
+
if (isGQLParseError2(ast)) {
|
|
10983
|
+
res.status(422).json({
|
|
10984
|
+
error: {
|
|
10985
|
+
code: ErrorCodes.INVALID_REQUEST,
|
|
10986
|
+
message: `GQL parse error: ${ast.message}`,
|
|
10987
|
+
hint: `Position: ${ast.pos}`,
|
|
10988
|
+
requestId: req.requestId,
|
|
10989
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
10990
|
+
}
|
|
10991
|
+
});
|
|
10992
|
+
return;
|
|
10993
|
+
}
|
|
10994
|
+
const plan = { type: ast.type, gql };
|
|
10995
|
+
if (ast.type === "FIND") {
|
|
10996
|
+
plan.steps = [
|
|
10997
|
+
{ step: 1, op: "SCAN_NODES", filter: ast.target === "*" ? "all" : `kind=${ast.target}` },
|
|
10998
|
+
...ast.where ? [{ step: 2, op: "WHERE", conditions: ast.where.exprs.length }] : [],
|
|
10999
|
+
...ast.limit !== void 0 ? [{ step: 3, op: "LIMIT", value: ast.limit }] : []
|
|
11000
|
+
];
|
|
11001
|
+
plan.estimatedCost = graph.size.nodes;
|
|
11002
|
+
} else if (ast.type === "TRAVERSE") {
|
|
11003
|
+
plan.steps = [
|
|
11004
|
+
{ step: 1, op: "FIND_START_NODE", name: ast.from },
|
|
11005
|
+
{ step: 2, op: "BFS", edgeKind: ast.edgeKind, maxDepth: ast.depth ?? 5 }
|
|
11006
|
+
];
|
|
11007
|
+
plan.estimatedCost = Math.min(graph.size.nodes, Math.pow(4, ast.depth ?? 5));
|
|
11008
|
+
} else if (ast.type === "PATH") {
|
|
11009
|
+
plan.steps = [
|
|
11010
|
+
{ step: 1, op: "FIND_NODES", from: ast.from, to: ast.to },
|
|
11011
|
+
{ step: 2, op: "BFS_SHORTEST_PATH" }
|
|
11012
|
+
];
|
|
11013
|
+
plan.estimatedCost = graph.size.nodes + graph.size.edges;
|
|
11014
|
+
} else if (ast.type === "COUNT") {
|
|
11015
|
+
plan.steps = [
|
|
11016
|
+
{ step: 1, op: "SCAN_NODES", filter: ast.target === "*" ? "all" : `kind=${ast.target}` },
|
|
11017
|
+
...ast.where ? [{ step: 2, op: "WHERE", conditions: ast.where.exprs.length }] : [],
|
|
11018
|
+
...ast.groupBy ? [{ step: 3, op: "GROUP_BY", property: ast.groupBy }] : [{ step: 3, op: "COUNT" }]
|
|
11019
|
+
];
|
|
11020
|
+
plan.estimatedCost = graph.size.nodes;
|
|
11021
|
+
}
|
|
11022
|
+
res.json({ plan, graphSize: graph.size });
|
|
11023
|
+
} catch (err) {
|
|
11024
|
+
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() } });
|
|
11025
|
+
}
|
|
11026
|
+
});
|
|
8985
11027
|
if (fs19.existsSync(WEB_DIST)) {
|
|
8986
11028
|
app.use(express.static(WEB_DIST));
|
|
8987
11029
|
app.get("/{*path}", (_req, res) => {
|
|
8988
|
-
res.sendFile(
|
|
11030
|
+
res.sendFile(path27.join(WEB_DIST, "index.html"));
|
|
8989
11031
|
});
|
|
8990
11032
|
}
|
|
8991
11033
|
app.use("/admin", requireRole("admin"));
|