@esankhan3/code-search-mcp 0.0.1 → 0.1.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 +203 -263
- package/dist/resources/resources.js +3 -3
- package/dist/server.js +2 -2
- package/dist/tools/graph.js +1 -1
- package/dist/tools/index-tools.js +1 -1
- package/dist/tools/profile.js +2 -2
- package/dist/tools/search.js +1 -1
- package/package.json +16 -9
- package/dist/core/ast-graph-builder.js +0 -800
- package/dist/core/chunker.js +0 -368
- package/dist/core/claude-runner.js +0 -173
- package/dist/core/config.js +0 -59
- package/dist/core/cross-repo-detector.js +0 -887
- package/dist/core/embedder.js +0 -334
- package/dist/core/file-walker.js +0 -186
- package/dist/core/git-diff.js +0 -160
- package/dist/core/graph-metrics.js +0 -233
- package/dist/core/indexer.js +0 -552
- package/dist/core/project-graph-builder.js +0 -202
- package/dist/core/query-classifier.js +0 -144
- package/dist/core/query-router.js +0 -210
- package/dist/core/rag-evaluator.js +0 -213
- package/dist/core/repo-profiler.js +0 -383
- package/dist/core/reranker.js +0 -218
- package/dist/core/retriever.js +0 -242
- package/dist/core/semantic-edge-detector.js +0 -147
- package/dist/core/service-mesh-inferrer.js +0 -241
- package/dist/core/structural-hasher.js +0 -316
- package/dist/core/tree-sitter-parser.js +0 -539
- package/dist/core/types.js +0 -0
- package/dist/core/vector-store.js +0 -259
- package/dist/core/workspace-detector.js +0 -481
package/dist/core/reranker.js
DELETED
|
@@ -1,218 +0,0 @@
|
|
|
1
|
-
async function parallelMap(items, fn, concurrency) {
|
|
2
|
-
const results = new Array(items.length);
|
|
3
|
-
let nextIndex = 0;
|
|
4
|
-
const workers = Array.from({ length: Math.min(concurrency, items.length) }, async () => {
|
|
5
|
-
while (nextIndex < items.length) {
|
|
6
|
-
const i = nextIndex++;
|
|
7
|
-
results[i] = await fn(items[i], i);
|
|
8
|
-
}
|
|
9
|
-
});
|
|
10
|
-
await Promise.all(workers);
|
|
11
|
-
return results;
|
|
12
|
-
}
|
|
13
|
-
class OllamaReranker {
|
|
14
|
-
baseUrl;
|
|
15
|
-
model;
|
|
16
|
-
timeoutMs;
|
|
17
|
-
constructor(opts) {
|
|
18
|
-
this.baseUrl = opts?.baseUrl || process.env.OLLAMA_HOST || "http://localhost:11434";
|
|
19
|
-
this.model = opts?.model || process.env.RERANKER_MODEL || "qwen3:0.6b";
|
|
20
|
-
this.timeoutMs = opts?.timeoutMs ?? 3e4;
|
|
21
|
-
}
|
|
22
|
-
async scoreOne(query, doc, index) {
|
|
23
|
-
try {
|
|
24
|
-
const controller = new AbortController();
|
|
25
|
-
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
26
|
-
const response = await fetch(`${this.baseUrl}/api/chat`, {
|
|
27
|
-
method: "POST",
|
|
28
|
-
headers: { "Content-Type": "application/json" },
|
|
29
|
-
body: JSON.stringify({
|
|
30
|
-
model: this.model,
|
|
31
|
-
messages: [
|
|
32
|
-
{
|
|
33
|
-
role: "user",
|
|
34
|
-
content: `Given a code search query, evaluate whether the following code document is relevant. Answer ONLY "yes" or "no", nothing else.
|
|
35
|
-
|
|
36
|
-
Query: ${query}
|
|
37
|
-
|
|
38
|
-
Document:
|
|
39
|
-
${doc.slice(0, 1500)}
|
|
40
|
-
|
|
41
|
-
Relevant? /no_think`
|
|
42
|
-
}
|
|
43
|
-
],
|
|
44
|
-
stream: false,
|
|
45
|
-
options: { temperature: 0, num_predict: 10 }
|
|
46
|
-
}),
|
|
47
|
-
signal: controller.signal
|
|
48
|
-
});
|
|
49
|
-
clearTimeout(timer);
|
|
50
|
-
if (!response.ok) return { index, score: 0.5 };
|
|
51
|
-
const json = await response.json();
|
|
52
|
-
const raw = (json.message?.content ?? "").toLowerCase().trim();
|
|
53
|
-
const answer = raw.replace(/<think>[\s\S]*?<\/think>/g, "").trim();
|
|
54
|
-
let score = 0.5;
|
|
55
|
-
if (answer.startsWith("yes")) score = 1;
|
|
56
|
-
else if (answer.startsWith("no")) score = 0;
|
|
57
|
-
else if (raw.includes("yes")) score = 0.8;
|
|
58
|
-
else if (raw.includes("no")) score = 0.2;
|
|
59
|
-
return { index, score };
|
|
60
|
-
} catch {
|
|
61
|
-
return { index, score: 0.5 };
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
async rerank(query, documents, topN) {
|
|
65
|
-
if (documents.length === 0) return [];
|
|
66
|
-
const scores = [];
|
|
67
|
-
for (let i = 0; i < documents.length; i++) {
|
|
68
|
-
const result = await this.scoreOne(query, documents[i], i);
|
|
69
|
-
scores.push(result);
|
|
70
|
-
}
|
|
71
|
-
scores.sort((a, b) => b.score - a.score);
|
|
72
|
-
return topN ? scores.slice(0, topN) : scores;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
class CohereReranker {
|
|
76
|
-
async rerank(query, documents, topN) {
|
|
77
|
-
const apiKey = process.env.COHERE_API_KEY;
|
|
78
|
-
if (!apiKey) {
|
|
79
|
-
throw new Error("COHERE_API_KEY environment variable is not set");
|
|
80
|
-
}
|
|
81
|
-
const response = await fetch("https://api.cohere.com/v2/rerank", {
|
|
82
|
-
method: "POST",
|
|
83
|
-
headers: {
|
|
84
|
-
"Content-Type": "application/json",
|
|
85
|
-
Authorization: `Bearer ${apiKey}`
|
|
86
|
-
},
|
|
87
|
-
body: JSON.stringify({
|
|
88
|
-
model: "rerank-v3.5",
|
|
89
|
-
query,
|
|
90
|
-
documents,
|
|
91
|
-
top_n: topN ?? documents.length
|
|
92
|
-
})
|
|
93
|
-
});
|
|
94
|
-
if (!response.ok) {
|
|
95
|
-
const body = await response.text();
|
|
96
|
-
throw new Error(`Cohere rerank request failed (${response.status}): ${body}`);
|
|
97
|
-
}
|
|
98
|
-
const json = await response.json();
|
|
99
|
-
return json.results.map((r) => ({ index: r.index, score: r.relevance_score }));
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
class VoyageReranker {
|
|
103
|
-
async rerank(query, documents, topN) {
|
|
104
|
-
const apiKey = process.env.VOYAGE_API_KEY;
|
|
105
|
-
if (!apiKey) {
|
|
106
|
-
throw new Error("VOYAGE_API_KEY environment variable is not set");
|
|
107
|
-
}
|
|
108
|
-
const response = await fetch("https://api.voyageai.com/v1/rerank", {
|
|
109
|
-
method: "POST",
|
|
110
|
-
headers: {
|
|
111
|
-
"Content-Type": "application/json",
|
|
112
|
-
Authorization: `Bearer ${apiKey}`
|
|
113
|
-
},
|
|
114
|
-
body: JSON.stringify({
|
|
115
|
-
model: "rerank-2",
|
|
116
|
-
query,
|
|
117
|
-
documents,
|
|
118
|
-
top_k: topN ?? documents.length
|
|
119
|
-
})
|
|
120
|
-
});
|
|
121
|
-
if (!response.ok) {
|
|
122
|
-
const body = await response.text();
|
|
123
|
-
throw new Error(`Voyage rerank request failed (${response.status}): ${body}`);
|
|
124
|
-
}
|
|
125
|
-
const json = await response.json();
|
|
126
|
-
return json.data.map((r) => ({ index: r.index, score: r.relevance_score }));
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
class OpenAICompatibleReranker {
|
|
130
|
-
baseUrl;
|
|
131
|
-
model;
|
|
132
|
-
apiKey;
|
|
133
|
-
timeoutMs;
|
|
134
|
-
constructor(opts) {
|
|
135
|
-
this.baseUrl = opts?.baseUrl || process.env.CODE_SEARCH_RERANKER_BASE_URL || "";
|
|
136
|
-
this.model = opts?.model || process.env.CODE_SEARCH_RERANKER_MODEL || "";
|
|
137
|
-
this.apiKey = opts?.apiKey || process.env.CODE_SEARCH_RERANKER_API_KEY;
|
|
138
|
-
this.timeoutMs = opts?.timeoutMs ?? 3e4;
|
|
139
|
-
if (!this.baseUrl) throw new Error("Reranker base URL required. Set CODE_SEARCH_RERANKER_BASE_URL");
|
|
140
|
-
if (!this.model) throw new Error("Reranker model required. Set CODE_SEARCH_RERANKER_MODEL");
|
|
141
|
-
}
|
|
142
|
-
async scoreOne(query, doc, index) {
|
|
143
|
-
try {
|
|
144
|
-
const controller = new AbortController();
|
|
145
|
-
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
146
|
-
const headers = { "Content-Type": "application/json" };
|
|
147
|
-
if (this.apiKey) headers["Authorization"] = `Bearer ${this.apiKey}`;
|
|
148
|
-
const response = await fetch(`${this.baseUrl.replace(/\/$/, "")}/v1/chat/completions`, {
|
|
149
|
-
method: "POST",
|
|
150
|
-
headers,
|
|
151
|
-
body: JSON.stringify({
|
|
152
|
-
model: this.model,
|
|
153
|
-
messages: [
|
|
154
|
-
{
|
|
155
|
-
role: "user",
|
|
156
|
-
content: `Given a code search query, evaluate whether the following code document is relevant. Answer ONLY "yes" or "no", nothing else.
|
|
157
|
-
|
|
158
|
-
Query: ${query}
|
|
159
|
-
|
|
160
|
-
Document:
|
|
161
|
-
${doc.slice(0, 1500)}
|
|
162
|
-
|
|
163
|
-
Relevant?`
|
|
164
|
-
}
|
|
165
|
-
],
|
|
166
|
-
temperature: 0,
|
|
167
|
-
max_tokens: 5
|
|
168
|
-
}),
|
|
169
|
-
signal: controller.signal
|
|
170
|
-
});
|
|
171
|
-
clearTimeout(timer);
|
|
172
|
-
if (!response.ok) return { index, score: 0.5 };
|
|
173
|
-
const json = await response.json();
|
|
174
|
-
const answer = (json.choices?.[0]?.message?.content ?? "").toLowerCase().trim();
|
|
175
|
-
let score = 0.5;
|
|
176
|
-
if (answer.startsWith("yes")) score = 1;
|
|
177
|
-
else if (answer.startsWith("no")) score = 0;
|
|
178
|
-
else if (answer.includes("yes")) score = 0.8;
|
|
179
|
-
else if (answer.includes("no")) score = 0.2;
|
|
180
|
-
return { index, score };
|
|
181
|
-
} catch {
|
|
182
|
-
return { index, score: 0.5 };
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
async rerank(query, documents, topN) {
|
|
186
|
-
if (documents.length === 0) return [];
|
|
187
|
-
const scores = await parallelMap(
|
|
188
|
-
documents,
|
|
189
|
-
(doc, i) => this.scoreOne(query, doc, i),
|
|
190
|
-
5
|
|
191
|
-
);
|
|
192
|
-
scores.sort((a, b) => b.score - a.score);
|
|
193
|
-
return topN ? scores.slice(0, topN) : scores;
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
function createReranker(provider) {
|
|
197
|
-
switch (provider) {
|
|
198
|
-
case "ollama":
|
|
199
|
-
return new OllamaReranker();
|
|
200
|
-
case "cohere":
|
|
201
|
-
return new CohereReranker();
|
|
202
|
-
case "voyage":
|
|
203
|
-
return new VoyageReranker();
|
|
204
|
-
case "openai-compatible":
|
|
205
|
-
case "custom":
|
|
206
|
-
return new OpenAICompatibleReranker();
|
|
207
|
-
case "none":
|
|
208
|
-
return null;
|
|
209
|
-
default:
|
|
210
|
-
if (process.env.CODE_SEARCH_RERANKER_BASE_URL) {
|
|
211
|
-
return new OpenAICompatibleReranker();
|
|
212
|
-
}
|
|
213
|
-
return new OllamaReranker();
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
export {
|
|
217
|
-
createReranker
|
|
218
|
-
};
|
package/dist/core/retriever.js
DELETED
|
@@ -1,242 +0,0 @@
|
|
|
1
|
-
import { classifyQuery } from "./query-classifier.js";
|
|
2
|
-
const queryEmbeddingCache = /* @__PURE__ */ new Map();
|
|
3
|
-
const CACHE_MAX_SIZE = 128;
|
|
4
|
-
const CACHE_TTL_MS = 10 * 60 * 1e3;
|
|
5
|
-
function getCachedEmbedding(query) {
|
|
6
|
-
const entry = queryEmbeddingCache.get(query);
|
|
7
|
-
if (!entry) return null;
|
|
8
|
-
if (Date.now() - entry.timestamp > CACHE_TTL_MS) {
|
|
9
|
-
queryEmbeddingCache.delete(query);
|
|
10
|
-
return null;
|
|
11
|
-
}
|
|
12
|
-
return entry.embedding;
|
|
13
|
-
}
|
|
14
|
-
function cacheEmbedding(query, embedding) {
|
|
15
|
-
if (queryEmbeddingCache.size >= CACHE_MAX_SIZE) {
|
|
16
|
-
const oldest = queryEmbeddingCache.keys().next().value;
|
|
17
|
-
if (oldest !== void 0) queryEmbeddingCache.delete(oldest);
|
|
18
|
-
}
|
|
19
|
-
queryEmbeddingCache.set(query, { embedding, timestamp: Date.now() });
|
|
20
|
-
}
|
|
21
|
-
class HybridRetriever {
|
|
22
|
-
constructor(vectorStore, embedder, graph, config, reranker = null, queryRouter = null) {
|
|
23
|
-
this.config = config;
|
|
24
|
-
this.reranker = reranker;
|
|
25
|
-
this.queryRouter = queryRouter;
|
|
26
|
-
this.vectorStore = vectorStore;
|
|
27
|
-
this.embedder = embedder;
|
|
28
|
-
this.graph = graph;
|
|
29
|
-
}
|
|
30
|
-
// Expose for testing/debugging
|
|
31
|
-
vectorStore;
|
|
32
|
-
embedder;
|
|
33
|
-
graph;
|
|
34
|
-
async retrieve(query, opts) {
|
|
35
|
-
const maxChunks = opts?.maxChunks ?? this.config.maxChunks;
|
|
36
|
-
const maxTokens = opts?.maxTokens ?? this.config.maxTokens;
|
|
37
|
-
const mode = opts?.mode ?? "vector+bm25+graph";
|
|
38
|
-
const useVector = mode !== "bm25";
|
|
39
|
-
const useBm25 = mode === "bm25" || mode === "vector+bm25" || mode === "vector+bm25+graph";
|
|
40
|
-
const useGraph = mode === "vector+graph" || mode === "vector+bm25+graph";
|
|
41
|
-
const classification = classifyQuery(query);
|
|
42
|
-
const adaptiveWeights = classification.weights;
|
|
43
|
-
let filterRepos = opts?.repoFilter ?? opts?.repos;
|
|
44
|
-
if (!filterRepos && this.queryRouter) {
|
|
45
|
-
try {
|
|
46
|
-
const routeResult = await this.queryRouter.route(query);
|
|
47
|
-
if (routeResult.strategy === "filtered") {
|
|
48
|
-
filterRepos = routeResult.repos;
|
|
49
|
-
}
|
|
50
|
-
} catch {
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
const filter = filterRepos ? `repoName IN (${filterRepos.map((r) => `'${r}'`).join(",")})` : void 0;
|
|
54
|
-
let queryEmbedding = null;
|
|
55
|
-
if (useVector) {
|
|
56
|
-
queryEmbedding = getCachedEmbedding(query);
|
|
57
|
-
if (!queryEmbedding) {
|
|
58
|
-
queryEmbedding = await this.embedder.embedSingle(query);
|
|
59
|
-
cacheEmbedding(query, queryEmbedding);
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
const [vectorResults, bm25Results] = await Promise.all([
|
|
63
|
-
useVector && queryEmbedding ? this.vectorStore.vectorSearch(queryEmbedding, { limit: 50, filter }) : Promise.resolve([]),
|
|
64
|
-
useBm25 ? this.vectorStore.fullTextSearch(query, 50) : Promise.resolve([])
|
|
65
|
-
]);
|
|
66
|
-
if (mode === "vector") {
|
|
67
|
-
const selected2 = packWithinBudget(vectorResults, maxTokens, maxChunks);
|
|
68
|
-
return { chunks: selected2, graphContext: "", totalTokens: sumTokens(selected2), query };
|
|
69
|
-
}
|
|
70
|
-
if (mode === "bm25") {
|
|
71
|
-
const selected2 = packWithinBudget(bm25Results, maxTokens, maxChunks);
|
|
72
|
-
return { chunks: selected2, graphContext: "", totalTokens: sumTokens(selected2), query };
|
|
73
|
-
}
|
|
74
|
-
const wV = adaptiveWeights.vector ?? this.config.hybridWeights.vector;
|
|
75
|
-
const wB = adaptiveWeights.bm25 ?? this.config.hybridWeights.bm25;
|
|
76
|
-
const retrievalSets = [];
|
|
77
|
-
const weights = [];
|
|
78
|
-
if (vectorResults.length > 0) {
|
|
79
|
-
retrievalSets.push(vectorResults);
|
|
80
|
-
weights.push(wV);
|
|
81
|
-
}
|
|
82
|
-
if (bm25Results.length > 0) {
|
|
83
|
-
retrievalSets.push(bm25Results);
|
|
84
|
-
weights.push(wB);
|
|
85
|
-
}
|
|
86
|
-
const fused = retrievalSets.length > 1 ? reciprocalRankFusion(retrievalSets, weights) : retrievalSets[0] ?? [];
|
|
87
|
-
let astChunks = [];
|
|
88
|
-
if (useGraph && this.graph) {
|
|
89
|
-
const seedNodeIds = this.resolveFusedSeeds(fused, 5);
|
|
90
|
-
if (seedNodeIds.length > 0) {
|
|
91
|
-
const expandedNodeIds = this.tripartiteExpand(seedNodeIds);
|
|
92
|
-
if (expandedNodeIds.length > 0) {
|
|
93
|
-
const lookups = expandedNodeIds.map((nodeId) => {
|
|
94
|
-
const parts = nodeId.split("::");
|
|
95
|
-
if (parts.length >= 3) {
|
|
96
|
-
return { repoName: parts[0], filePath: parts[1], entityName: parts.slice(2).join("::") };
|
|
97
|
-
}
|
|
98
|
-
return { repoName: parts[0], filePath: parts.slice(1).join("::") };
|
|
99
|
-
});
|
|
100
|
-
astChunks = await this.vectorStore.getChunksByEntity(lookups);
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
const candidatePool = deduplicateChunks([...fused.slice(0, 15), ...astChunks]);
|
|
105
|
-
let finalChunks;
|
|
106
|
-
if (this.reranker && candidatePool.length > 1) {
|
|
107
|
-
try {
|
|
108
|
-
const documents = candidatePool.map((sc) => sc.chunk.contextualizedContent || sc.chunk.content);
|
|
109
|
-
const ranked = await this.reranker.rerank(query, documents, maxChunks);
|
|
110
|
-
finalChunks = ranked.map((r) => ({
|
|
111
|
-
...candidatePool[r.index],
|
|
112
|
-
score: r.score,
|
|
113
|
-
source: "fused"
|
|
114
|
-
}));
|
|
115
|
-
} catch {
|
|
116
|
-
finalChunks = candidatePool;
|
|
117
|
-
}
|
|
118
|
-
} else {
|
|
119
|
-
finalChunks = candidatePool;
|
|
120
|
-
}
|
|
121
|
-
const selected = packWithinBudget(finalChunks, maxTokens, maxChunks);
|
|
122
|
-
const graphContext = useGraph && this.graph ? this.graph.exportForPrompt(2e3) ?? "" : "";
|
|
123
|
-
return {
|
|
124
|
-
chunks: selected,
|
|
125
|
-
graphContext,
|
|
126
|
-
totalTokens: sumTokens(selected),
|
|
127
|
-
query
|
|
128
|
-
};
|
|
129
|
-
}
|
|
130
|
-
// ---------------------------------------------------------------
|
|
131
|
-
// Seed resolution: map chunk entities to graph node IDs
|
|
132
|
-
// ---------------------------------------------------------------
|
|
133
|
-
resolveFusedSeeds(fused, maxSeeds) {
|
|
134
|
-
if (!this.graph) return [];
|
|
135
|
-
const g = this.graph.getGraph();
|
|
136
|
-
const seenFiles = /* @__PURE__ */ new Set();
|
|
137
|
-
const nodeIds = [];
|
|
138
|
-
for (const sc of fused) {
|
|
139
|
-
if (nodeIds.length >= maxSeeds) break;
|
|
140
|
-
const { repoName, entityName, filePath } = sc.chunk;
|
|
141
|
-
if (!entityName) continue;
|
|
142
|
-
if (seenFiles.has(filePath)) continue;
|
|
143
|
-
seenFiles.add(filePath);
|
|
144
|
-
const baseName = entityName.replace(/\$\d+$/, "");
|
|
145
|
-
const exactId = `${repoName}::${filePath}::${baseName}`;
|
|
146
|
-
if (g.hasNode(exactId)) {
|
|
147
|
-
nodeIds.push(exactId);
|
|
148
|
-
continue;
|
|
149
|
-
}
|
|
150
|
-
const exactIdOrig = `${repoName}::${filePath}::${entityName}`;
|
|
151
|
-
if (g.hasNode(exactIdOrig)) {
|
|
152
|
-
nodeIds.push(exactIdOrig);
|
|
153
|
-
continue;
|
|
154
|
-
}
|
|
155
|
-
let found = false;
|
|
156
|
-
g.forEachNode((nodeId, attrs) => {
|
|
157
|
-
if (!found && attrs.repo === repoName && attrs.label === baseName) {
|
|
158
|
-
nodeIds.push(nodeId);
|
|
159
|
-
found = true;
|
|
160
|
-
}
|
|
161
|
-
});
|
|
162
|
-
}
|
|
163
|
-
return nodeIds;
|
|
164
|
-
}
|
|
165
|
-
// ---------------------------------------------------------------
|
|
166
|
-
// Tripartite expansion: dependencies + dependents + definitions
|
|
167
|
-
// ---------------------------------------------------------------
|
|
168
|
-
tripartiteExpand(seedNodeIds) {
|
|
169
|
-
if (!this.graph) return [];
|
|
170
|
-
const g = this.graph.getGraph();
|
|
171
|
-
const expanded = /* @__PURE__ */ new Set();
|
|
172
|
-
const seedSet = new Set(seedNodeIds);
|
|
173
|
-
for (const seed of seedNodeIds) {
|
|
174
|
-
if (!g.hasNode(seed)) continue;
|
|
175
|
-
g.forEachOutEdge(seed, (_edge, attrs, _src, tgt) => {
|
|
176
|
-
const conf = attrs.confidence ?? 0.8;
|
|
177
|
-
if (conf >= 0.7 && attrs.type !== "contains" && !seedSet.has(tgt)) {
|
|
178
|
-
expanded.add(tgt);
|
|
179
|
-
}
|
|
180
|
-
});
|
|
181
|
-
g.forEachInEdge(seed, (_edge, attrs, src) => {
|
|
182
|
-
const conf = attrs.confidence ?? 0.8;
|
|
183
|
-
if (conf >= 0.7 && attrs.type !== "contains" && !seedSet.has(src)) {
|
|
184
|
-
expanded.add(src);
|
|
185
|
-
}
|
|
186
|
-
});
|
|
187
|
-
}
|
|
188
|
-
return [...expanded].filter((nodeId) => {
|
|
189
|
-
const type = g.getNodeAttributes(nodeId)?.type;
|
|
190
|
-
return type !== "module" && type !== "package";
|
|
191
|
-
});
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
function reciprocalRankFusion(resultSets, weights, k = 60) {
|
|
195
|
-
const scoreMap = /* @__PURE__ */ new Map();
|
|
196
|
-
for (let setIdx = 0; setIdx < resultSets.length; setIdx++) {
|
|
197
|
-
const results = resultSets[setIdx];
|
|
198
|
-
const weight = weights[setIdx] ?? 1;
|
|
199
|
-
for (let rank = 0; rank < results.length; rank++) {
|
|
200
|
-
const { chunk } = results[rank];
|
|
201
|
-
const rrfScore = weight * (1 / (k + rank + 1));
|
|
202
|
-
const existing = scoreMap.get(chunk.id);
|
|
203
|
-
if (existing) {
|
|
204
|
-
existing.score += rrfScore;
|
|
205
|
-
} else {
|
|
206
|
-
scoreMap.set(chunk.id, { chunk, score: rrfScore });
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
return Array.from(scoreMap.values()).sort((a, b) => b.score - a.score).map(({ chunk, score }) => ({
|
|
211
|
-
chunk,
|
|
212
|
-
score,
|
|
213
|
-
source: "fused"
|
|
214
|
-
}));
|
|
215
|
-
}
|
|
216
|
-
function deduplicateChunks(chunks) {
|
|
217
|
-
const seen = /* @__PURE__ */ new Set();
|
|
218
|
-
const result = [];
|
|
219
|
-
for (const sc of chunks) {
|
|
220
|
-
if (seen.has(sc.chunk.id)) continue;
|
|
221
|
-
seen.add(sc.chunk.id);
|
|
222
|
-
result.push(sc);
|
|
223
|
-
}
|
|
224
|
-
return result;
|
|
225
|
-
}
|
|
226
|
-
function packWithinBudget(chunks, maxTokens, maxChunks) {
|
|
227
|
-
const selected = [];
|
|
228
|
-
let totalTokens = 0;
|
|
229
|
-
for (const sc of chunks) {
|
|
230
|
-
if (maxChunks && selected.length >= maxChunks) break;
|
|
231
|
-
if (totalTokens + sc.chunk.tokens > maxTokens) break;
|
|
232
|
-
selected.push(sc);
|
|
233
|
-
totalTokens += sc.chunk.tokens;
|
|
234
|
-
}
|
|
235
|
-
return selected;
|
|
236
|
-
}
|
|
237
|
-
function sumTokens(chunks) {
|
|
238
|
-
return chunks.reduce((sum, sc) => sum + sc.chunk.tokens, 0);
|
|
239
|
-
}
|
|
240
|
-
export {
|
|
241
|
-
HybridRetriever
|
|
242
|
-
};
|
|
@@ -1,147 +0,0 @@
|
|
|
1
|
-
import { readFileSync } from "node:fs";
|
|
2
|
-
import { join } from "node:path";
|
|
3
|
-
const DI_EVENT_PATTERNS = [
|
|
4
|
-
// DI patterns
|
|
5
|
-
/\@Injectable|\@Inject|\@Autowired/,
|
|
6
|
-
/container\.(?:register|bind|singleton|resolve)/,
|
|
7
|
-
/\$this->app->(?:bind|singleton|make)/,
|
|
8
|
-
/providers:\s*\[/,
|
|
9
|
-
// Event patterns
|
|
10
|
-
/\.on\s*\(|\.emit\s*\(|\.addEventListener/,
|
|
11
|
-
/EventEmitter|@Subscribe|@Listener|@EventHandler/,
|
|
12
|
-
/event\s*\(\s*['"`]/,
|
|
13
|
-
// Routing patterns
|
|
14
|
-
/\@Controller|\@Get|\@Post|\@Put|\@Delete|\@RequestMapping/,
|
|
15
|
-
/app\.(?:get|post|put|delete|use)\s*\(/,
|
|
16
|
-
/router\.(?:get|post|put|delete)\s*\(/,
|
|
17
|
-
// Factory/Registry patterns
|
|
18
|
-
/\.register\s*\(|\.factory\s*\(/,
|
|
19
|
-
/switch\s*\([^)]*type[^)]*\)/i
|
|
20
|
-
];
|
|
21
|
-
function fileMatchesDIPatterns(content) {
|
|
22
|
-
return DI_EVENT_PATTERNS.some((p) => p.test(content));
|
|
23
|
-
}
|
|
24
|
-
function findOrphanEntityFiles(graph) {
|
|
25
|
-
const outgoing = /* @__PURE__ */ new Map();
|
|
26
|
-
for (const edge of graph.links) {
|
|
27
|
-
if (edge.type === "contains") continue;
|
|
28
|
-
outgoing.set(edge.source, (outgoing.get(edge.source) ?? 0) + 1);
|
|
29
|
-
}
|
|
30
|
-
const orphanFiles = /* @__PURE__ */ new Set();
|
|
31
|
-
for (const node of graph.nodes) {
|
|
32
|
-
if (node.type === "module" || node.type === "package") continue;
|
|
33
|
-
if ((outgoing.get(node.id) ?? 0) === 0 && node.file) {
|
|
34
|
-
orphanFiles.add(node.file);
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
return orphanFiles;
|
|
38
|
-
}
|
|
39
|
-
const SEMANTIC_EDGE_PROMPT = `You are analyzing source code to find structural relationships that static AST analysis missed.
|
|
40
|
-
|
|
41
|
-
Given code files and the list of known entities, identify relationships of these types ONLY:
|
|
42
|
-
1. **di-inject**: A class receives another class via constructor injection, decorator, or container registration
|
|
43
|
-
2. **event-wire**: An entity emits/publishes an event that another entity subscribes to/handles
|
|
44
|
-
3. **config-route**: A config file or decorator maps a path/key to a handler function
|
|
45
|
-
4. **dynamic-dispatch**: A factory/registry pattern where a string key maps to a class/function
|
|
46
|
-
|
|
47
|
-
Respond with ONLY valid JSON (no markdown, no explanation):
|
|
48
|
-
{
|
|
49
|
-
"edges": [
|
|
50
|
-
{
|
|
51
|
-
"source": "file/path.ext::EntityName",
|
|
52
|
-
"target": "file/path.ext::EntityName",
|
|
53
|
-
"type": "di-inject",
|
|
54
|
-
"evidence": "brief explanation",
|
|
55
|
-
"confidence": 0.8
|
|
56
|
-
}
|
|
57
|
-
]
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
Rules:
|
|
61
|
-
- ONLY report edges between entities in the Known Entities list below
|
|
62
|
-
- ONLY report edges with confidence >= 0.7
|
|
63
|
-
- Do NOT report edges detectable by import analysis or direct function calls
|
|
64
|
-
- If no relationships found, return {"edges": []}
|
|
65
|
-
|
|
66
|
-
## Known Entities
|
|
67
|
-
{ENTITY_LIST}
|
|
68
|
-
|
|
69
|
-
## Code Files
|
|
70
|
-
{FILE_CONTENTS}`;
|
|
71
|
-
async function detectSemanticEdges(repoPath, graph, callLLM, opts) {
|
|
72
|
-
const maxFiles = opts?.maxFiles ?? 30;
|
|
73
|
-
const allEdges = [];
|
|
74
|
-
let totalTokens = 0;
|
|
75
|
-
const orphanFiles = findOrphanEntityFiles(graph);
|
|
76
|
-
const patternFiles = /* @__PURE__ */ new Set();
|
|
77
|
-
for (const node of graph.nodes) {
|
|
78
|
-
if (node.type !== "module" || !node.file) continue;
|
|
79
|
-
try {
|
|
80
|
-
const content = readFileSync(join(repoPath, node.file), "utf-8");
|
|
81
|
-
if (fileMatchesDIPatterns(content)) {
|
|
82
|
-
patternFiles.add(node.file);
|
|
83
|
-
}
|
|
84
|
-
} catch {
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
const candidateFiles = [.../* @__PURE__ */ new Set([...orphanFiles, ...patternFiles])].slice(0, maxFiles);
|
|
88
|
-
if (candidateFiles.length === 0) return { edges: [], filesAnalyzed: 0, tokensUsed: 0 };
|
|
89
|
-
const nodeSet = new Set(graph.nodes.map((n) => n.id));
|
|
90
|
-
const entityList = graph.nodes.filter((n) => n.type !== "module" && n.type !== "package").map((n) => `${n.id} (${n.type})`).join("\n");
|
|
91
|
-
const batches = [];
|
|
92
|
-
let currentBatch = [];
|
|
93
|
-
let currentSize = 0;
|
|
94
|
-
for (const relPath of candidateFiles) {
|
|
95
|
-
try {
|
|
96
|
-
const content = readFileSync(join(repoPath, relPath), "utf-8");
|
|
97
|
-
const trimmed = content.slice(0, 3e3);
|
|
98
|
-
if (currentSize + trimmed.length > 12e3) {
|
|
99
|
-
if (currentBatch.length > 0) batches.push(currentBatch);
|
|
100
|
-
currentBatch = [];
|
|
101
|
-
currentSize = 0;
|
|
102
|
-
}
|
|
103
|
-
currentBatch.push({ relPath, content: trimmed });
|
|
104
|
-
currentSize += trimmed.length;
|
|
105
|
-
} catch {
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
if (currentBatch.length > 0) batches.push(currentBatch);
|
|
109
|
-
for (const batch of batches) {
|
|
110
|
-
const fileContents = batch.map((f) => `### ${f.relPath}
|
|
111
|
-
\`\`\`
|
|
112
|
-
${f.content}
|
|
113
|
-
\`\`\``).join("\n\n");
|
|
114
|
-
const prompt = SEMANTIC_EDGE_PROMPT.replace("{ENTITY_LIST}", entityList.slice(0, 5e3)).replace("{FILE_CONTENTS}", fileContents);
|
|
115
|
-
try {
|
|
116
|
-
const result = await callLLM(prompt);
|
|
117
|
-
totalTokens += result.inputTokens + result.outputTokens;
|
|
118
|
-
let jsonStr = result.content.trim();
|
|
119
|
-
const jsonMatch = jsonStr.match(/```(?:json)?\s*([\s\S]*?)```/);
|
|
120
|
-
if (jsonMatch) jsonStr = jsonMatch[1].trim();
|
|
121
|
-
const parsed = JSON.parse(jsonStr);
|
|
122
|
-
if (!Array.isArray(parsed?.edges)) continue;
|
|
123
|
-
for (const edge of parsed.edges) {
|
|
124
|
-
if (nodeSet.has(edge.source) && nodeSet.has(edge.target) && edge.source !== edge.target) {
|
|
125
|
-
const confidence = typeof edge.confidence === "number" ? Math.min(edge.confidence, 0.95) : 0.75;
|
|
126
|
-
if (confidence >= 0.7) {
|
|
127
|
-
allEdges.push({
|
|
128
|
-
source: edge.source,
|
|
129
|
-
target: edge.target,
|
|
130
|
-
type: edge.type || "semantic",
|
|
131
|
-
confidence
|
|
132
|
-
});
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
} catch {
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
return {
|
|
140
|
-
edges: allEdges,
|
|
141
|
-
filesAnalyzed: candidateFiles.length,
|
|
142
|
-
tokensUsed: totalTokens
|
|
143
|
-
};
|
|
144
|
-
}
|
|
145
|
-
export {
|
|
146
|
-
detectSemanticEdges
|
|
147
|
-
};
|