dev-mcp-server 0.0.2
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/.env.example +68 -0
- package/README.md +333 -0
- package/cli.js +248 -0
- package/package.json +60 -0
- package/src/api/routes/ingest.js +69 -0
- package/src/api/routes/knowledge.js +65 -0
- package/src/api/routes/query.js +105 -0
- package/src/api/server.js +91 -0
- package/src/core/indexer.js +171 -0
- package/src/core/ingester.js +155 -0
- package/src/core/queryEngine.js +236 -0
- package/src/storage/store.js +125 -0
- package/src/utils/fileParser.js +183 -0
- package/src/utils/llmClient.js +206 -0
- package/src/utils/logger.js +28 -0
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "dev-mcp-server",
|
|
3
|
+
"version": "0.0.2",
|
|
4
|
+
"description": "Model Context Platform — AI that understands YOUR codebase",
|
|
5
|
+
"main": "src/api/server.js",
|
|
6
|
+
"type": "commonjs",
|
|
7
|
+
"author": {
|
|
8
|
+
"name": "Anand Pilania",
|
|
9
|
+
"email": "pilaniaanand@gmail.com",
|
|
10
|
+
"url": "https://github.com/AnandPilania"
|
|
11
|
+
},
|
|
12
|
+
"bin": {
|
|
13
|
+
"dev-mcp": "./cli.js",
|
|
14
|
+
"dev-mcp-server": "./cli.js"
|
|
15
|
+
},
|
|
16
|
+
"scripts": {
|
|
17
|
+
"start": "node src/api/server.js",
|
|
18
|
+
"dev": "nodemon src/api/server.js",
|
|
19
|
+
"ingest": "node cli.js ingest",
|
|
20
|
+
"query": "node cli.js query",
|
|
21
|
+
"stats": "node cli.js stats",
|
|
22
|
+
"clear": "node cli.js clear"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@anthropic-ai/sdk": "^0.24.0",
|
|
26
|
+
"chalk": "^4.1.2",
|
|
27
|
+
"commander": "^12.0.0",
|
|
28
|
+
"cors": "^2.8.5",
|
|
29
|
+
"dotenv": "^16.4.5",
|
|
30
|
+
"express": "^4.19.2",
|
|
31
|
+
"glob": "^10.4.1",
|
|
32
|
+
"morgan": "^1.10.0",
|
|
33
|
+
"natural": "^8.0.1",
|
|
34
|
+
"openai": "^4.52.0",
|
|
35
|
+
"ora": "^5.4.1",
|
|
36
|
+
"winston": "^3.13.0"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"nodemon": "^3.1.2"
|
|
40
|
+
},
|
|
41
|
+
"engines": {
|
|
42
|
+
"node": ">=18.0.0"
|
|
43
|
+
},
|
|
44
|
+
"files": [
|
|
45
|
+
"cli.js",
|
|
46
|
+
"src/",
|
|
47
|
+
".env.example",
|
|
48
|
+
"README.md"
|
|
49
|
+
],
|
|
50
|
+
"keywords": [
|
|
51
|
+
"mcp",
|
|
52
|
+
"codebase",
|
|
53
|
+
"ai",
|
|
54
|
+
"developer-tools",
|
|
55
|
+
"anthropic",
|
|
56
|
+
"ollama",
|
|
57
|
+
"azure-openai"
|
|
58
|
+
],
|
|
59
|
+
"license": "MIT"
|
|
60
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
const express = require('express');
|
|
2
|
+
const router = express.Router();
|
|
3
|
+
const ingester = require('../../core/ingester');
|
|
4
|
+
const indexer = require('../../core/indexer');
|
|
5
|
+
const store = require('../../storage/store');
|
|
6
|
+
const logger = require('../../utils/logger');
|
|
7
|
+
|
|
8
|
+
router.post('/file', async (req, res) => {
|
|
9
|
+
const { filePath } = req.body;
|
|
10
|
+
|
|
11
|
+
if (!filePath) {
|
|
12
|
+
return res.status(400).json({ error: 'filePath is required' });
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
const result = await ingester.ingestFile(filePath);
|
|
17
|
+
indexer.build();
|
|
18
|
+
res.json({ success: true, result });
|
|
19
|
+
} catch (err) {
|
|
20
|
+
logger.error(`Ingest file error: ${err.message}`);
|
|
21
|
+
res.status(500).json({ error: err.message });
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
router.post('/directory', async (req, res) => {
|
|
26
|
+
const { dirPath } = req.body;
|
|
27
|
+
|
|
28
|
+
if (!dirPath) {
|
|
29
|
+
return res.status(400).json({ error: 'dirPath is required' });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
const result = await ingester.ingestDirectory(dirPath);
|
|
34
|
+
res.json({ success: true, result });
|
|
35
|
+
} catch (err) {
|
|
36
|
+
logger.error(`Ingest directory error: ${err.message}`);
|
|
37
|
+
res.status(500).json({ error: err.message });
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
router.post('/raw', async (req, res) => {
|
|
42
|
+
const { content, kind, label, tags } = req.body;
|
|
43
|
+
|
|
44
|
+
if (!content) {
|
|
45
|
+
return res.status(400).json({ error: 'content is required' });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
const result = await ingester.ingestRawText(content, { kind, label, tags });
|
|
50
|
+
indexer.build();
|
|
51
|
+
res.json({ success: true, result });
|
|
52
|
+
} catch (err) {
|
|
53
|
+
logger.error(`Ingest raw error: ${err.message}`);
|
|
54
|
+
res.status(500).json({ error: err.message });
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
router.delete('/clear', (req, res) => {
|
|
59
|
+
store.clear();
|
|
60
|
+
indexer.invalidate();
|
|
61
|
+
res.json({ success: true, message: 'Knowledge base cleared' });
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
router.get('/files', (req, res) => {
|
|
65
|
+
const files = store.getIngestedFiles();
|
|
66
|
+
res.json({ files, count: files.length });
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
module.exports = router;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
const express = require('express');
|
|
2
|
+
const router = express.Router();
|
|
3
|
+
const store = require('../../storage/store');
|
|
4
|
+
const indexer = require('../../core/indexer');
|
|
5
|
+
|
|
6
|
+
router.get('/stats', (req, res) => {
|
|
7
|
+
const stats = store.getStats();
|
|
8
|
+
res.json(stats);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
router.get('/search', (req, res) => {
|
|
12
|
+
const { q, topK = '8', kind } = req.query;
|
|
13
|
+
|
|
14
|
+
if (!q) {
|
|
15
|
+
return res.status(400).json({ error: 'q query parameter is required' });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const filter = kind ? { kind } : {};
|
|
19
|
+
const results = indexer.search(q, parseInt(topK), filter);
|
|
20
|
+
|
|
21
|
+
res.json({
|
|
22
|
+
query: q,
|
|
23
|
+
count: results.length,
|
|
24
|
+
results: results.map(r => ({
|
|
25
|
+
file: r.filename,
|
|
26
|
+
path: r.filePath,
|
|
27
|
+
kind: r.kind,
|
|
28
|
+
relevanceScore: r.relevanceScore,
|
|
29
|
+
snippet: r.content.slice(0, 300) + (r.content.length > 300 ? '...' : ''),
|
|
30
|
+
metadata: r.metadata,
|
|
31
|
+
})),
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
router.get('/files', (req, res) => {
|
|
36
|
+
const { kind } = req.query;
|
|
37
|
+
const docs = kind ? store.getByKind(kind) : store.getAll();
|
|
38
|
+
|
|
39
|
+
const grouped = {};
|
|
40
|
+
for (const doc of docs) {
|
|
41
|
+
if (!grouped[doc.filePath]) {
|
|
42
|
+
grouped[doc.filePath] = {
|
|
43
|
+
filePath: doc.filePath,
|
|
44
|
+
filename: doc.filename,
|
|
45
|
+
kind: doc.kind,
|
|
46
|
+
chunks: 0,
|
|
47
|
+
ingestedAt: doc.ingestedAt,
|
|
48
|
+
metadata: doc.metadata,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
grouped[doc.filePath].chunks++;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
res.json({
|
|
55
|
+
count: Object.keys(grouped).length,
|
|
56
|
+
files: Object.values(grouped),
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
router.post('/rebuild', (req, res) => {
|
|
61
|
+
const count = indexer.build();
|
|
62
|
+
res.json({ success: true, documentsIndexed: count });
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
module.exports = router;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
const express = require('express');
|
|
2
|
+
const router = express.Router();
|
|
3
|
+
const { QueryEngine, QUERY_MODES } = require('../../core/queryEngine');
|
|
4
|
+
const logger = require('../../utils/logger');
|
|
5
|
+
|
|
6
|
+
router.post('/', async (req, res) => {
|
|
7
|
+
const { question, topK, filter, mode } = req.body;
|
|
8
|
+
|
|
9
|
+
if (!question) {
|
|
10
|
+
return res.status(400).json({ error: 'question is required' });
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
const result = await QueryEngine.query(question, { topK, filter, mode });
|
|
15
|
+
res.json(result);
|
|
16
|
+
} catch (err) {
|
|
17
|
+
logger.error(`Query error: ${err.message}`);
|
|
18
|
+
res.status(500).json({ error: err.message });
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
router.post('/debug', async (req, res) => {
|
|
23
|
+
const { error, stackTrace, question, topK } = req.body;
|
|
24
|
+
|
|
25
|
+
if (!error && !question) {
|
|
26
|
+
return res.status(400).json({ error: 'error or question is required' });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
let result;
|
|
31
|
+
if (error) {
|
|
32
|
+
result = await QueryEngine.debugError(error, stackTrace, { topK });
|
|
33
|
+
} else {
|
|
34
|
+
result = await QueryEngine.query(question, { mode: QUERY_MODES.DEBUG, topK });
|
|
35
|
+
}
|
|
36
|
+
res.json(result);
|
|
37
|
+
} catch (err) {
|
|
38
|
+
logger.error(`Debug query error: ${err.message}`);
|
|
39
|
+
res.status(500).json({ error: err.message });
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
router.post('/usage', async (req, res) => {
|
|
44
|
+
const { symbol, topK } = req.body;
|
|
45
|
+
|
|
46
|
+
if (!symbol) {
|
|
47
|
+
return res.status(400).json({ error: 'symbol is required' });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
const result = await QueryEngine.findUsages(symbol, { topK });
|
|
52
|
+
res.json(result);
|
|
53
|
+
} catch (err) {
|
|
54
|
+
logger.error(`Usage query error: ${err.message}`);
|
|
55
|
+
res.status(500).json({ error: err.message });
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
router.post('/impact', async (req, res) => {
|
|
60
|
+
const { target, changeDescription, topK } = req.body;
|
|
61
|
+
|
|
62
|
+
if (!target) {
|
|
63
|
+
return res.status(400).json({ error: 'target is required' });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
const result = await QueryEngine.analyzeImpact(target, changeDescription, { topK });
|
|
68
|
+
res.json(result);
|
|
69
|
+
} catch (err) {
|
|
70
|
+
logger.error(`Impact query error: ${err.message}`);
|
|
71
|
+
res.status(500).json({ error: err.message });
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
router.post('/stream', async (req, res) => {
|
|
76
|
+
const { question, topK, mode } = req.body;
|
|
77
|
+
|
|
78
|
+
if (!question) {
|
|
79
|
+
return res.status(400).json({ error: 'question is required' });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
res.setHeader('Content-Type', 'text/event-stream');
|
|
83
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
84
|
+
res.setHeader('Connection', 'keep-alive');
|
|
85
|
+
res.flushHeaders();
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
const generator = await QueryEngine.query(question, { topK, mode, stream: true });
|
|
89
|
+
|
|
90
|
+
for await (const chunk of generator) {
|
|
91
|
+
res.write(`data: ${JSON.stringify(chunk)}\n\n`);
|
|
92
|
+
|
|
93
|
+
if (chunk.type === 'done') {
|
|
94
|
+
res.end();
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
} catch (err) {
|
|
99
|
+
logger.error(`Stream query error: ${err.message}`);
|
|
100
|
+
res.write(`data: ${JSON.stringify({ type: 'error', error: err.message })}\n\n`);
|
|
101
|
+
res.end();
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
module.exports = router;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
require('dotenv').config({ path: path.resolve(process.cwd(), '.env') });
|
|
3
|
+
const express = require('express');
|
|
4
|
+
const cors = require('cors');
|
|
5
|
+
const morgan = require('morgan');
|
|
6
|
+
const logger = require('../utils/logger');
|
|
7
|
+
const indexer = require('../core/indexer');
|
|
8
|
+
const store = require('../storage/store');
|
|
9
|
+
|
|
10
|
+
const ingestRoutes = require('./routes/ingest');
|
|
11
|
+
const queryRoutes = require('./routes/query');
|
|
12
|
+
const knowledgeRoutes = require('./routes/knowledge');
|
|
13
|
+
|
|
14
|
+
const app = express();
|
|
15
|
+
const PORT = process.env.PORT || 3000;
|
|
16
|
+
|
|
17
|
+
app.use(cors());
|
|
18
|
+
app.use(express.json({ limit: '10mb' }));
|
|
19
|
+
app.use(morgan('dev', {
|
|
20
|
+
stream: { write: (msg) => logger.info(msg.trim()) },
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
app.use('/api/ingest', ingestRoutes);
|
|
24
|
+
app.use('/api/query', queryRoutes);
|
|
25
|
+
app.use('/api/knowledge', knowledgeRoutes);
|
|
26
|
+
|
|
27
|
+
app.get('/health', (req, res) => {
|
|
28
|
+
const stats = store.getStats();
|
|
29
|
+
res.json({
|
|
30
|
+
status: 'ok',
|
|
31
|
+
uptime: process.uptime(),
|
|
32
|
+
memory: process.memoryUsage(),
|
|
33
|
+
knowledgeBase: stats,
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
app.get('/', (req, res) => {
|
|
38
|
+
res.json({
|
|
39
|
+
name: 'Dev MCP Server — Model Context Platform',
|
|
40
|
+
version: '1.0.0',
|
|
41
|
+
description: 'AI that understands YOUR codebase',
|
|
42
|
+
endpoints: {
|
|
43
|
+
health: 'GET /health',
|
|
44
|
+
ingest: {
|
|
45
|
+
file: 'POST /api/ingest/file',
|
|
46
|
+
directory: 'POST /api/ingest/directory',
|
|
47
|
+
raw: 'POST /api/ingest/raw',
|
|
48
|
+
clear: 'DELETE /api/ingest/clear',
|
|
49
|
+
listFiles: 'GET /api/ingest/files',
|
|
50
|
+
},
|
|
51
|
+
query: {
|
|
52
|
+
general: 'POST /api/query',
|
|
53
|
+
debug: 'POST /api/query/debug',
|
|
54
|
+
usage: 'POST /api/query/usage',
|
|
55
|
+
impact: 'POST /api/query/impact',
|
|
56
|
+
stream: 'POST /api/query/stream',
|
|
57
|
+
},
|
|
58
|
+
knowledge: {
|
|
59
|
+
stats: 'GET /api/knowledge/stats',
|
|
60
|
+
search: 'GET /api/knowledge/search?q=<query>',
|
|
61
|
+
files: 'GET /api/knowledge/files',
|
|
62
|
+
rebuild: 'POST /api/knowledge/rebuild',
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
app.use((req, res) => {
|
|
69
|
+
res.status(404).json({ error: `Route not found: ${req.method} ${req.path}` });
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
app.use((err, req, res, next) => {
|
|
73
|
+
logger.error(`Unhandled error: ${err.message}`);
|
|
74
|
+
res.status(500).json({ error: 'Internal server error', detail: err.message });
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
app.listen(PORT, () => {
|
|
78
|
+
logger.info(`🚀 Dev MCP Server running on http://localhost:${PORT}`);
|
|
79
|
+
logger.info(`📚 Knowledge base: ${store.getStats().totalDocs} documents`);
|
|
80
|
+
|
|
81
|
+
const stats = store.getStats();
|
|
82
|
+
if (stats.totalDocs > 0) {
|
|
83
|
+
logger.info('🔍 Rebuilding search index...');
|
|
84
|
+
const count = indexer.build();
|
|
85
|
+
logger.info(`✅ Index ready: ${count} documents`);
|
|
86
|
+
} else {
|
|
87
|
+
logger.info('📭 Knowledge base is empty — run: node cli.js ingest <path>');
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
module.exports = app;
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
const natural = require('natural');
|
|
2
|
+
const store = require('../storage/store');
|
|
3
|
+
const logger = require('../utils/logger');
|
|
4
|
+
|
|
5
|
+
const tokenizer = new natural.WordTokenizer();
|
|
6
|
+
const TfIdf = natural.TfIdf;
|
|
7
|
+
|
|
8
|
+
class Indexer {
|
|
9
|
+
constructor() {
|
|
10
|
+
this.tfidf = new TfIdf();
|
|
11
|
+
this._docMap = [];
|
|
12
|
+
this._built = false;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
build() {
|
|
16
|
+
this.tfidf = new TfIdf();
|
|
17
|
+
this._docMap = [];
|
|
18
|
+
|
|
19
|
+
const docs = store.getAll();
|
|
20
|
+
for (const doc of docs) {
|
|
21
|
+
const text = this._docToText(doc);
|
|
22
|
+
this.tfidf.addDocument(text);
|
|
23
|
+
this._docMap.push(doc.id);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
this._built = true;
|
|
27
|
+
logger.info(`Index built: ${docs.length} documents`);
|
|
28
|
+
return docs.length;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
search(query, topK = 8, filter = {}) {
|
|
32
|
+
if (!this._built || this._docMap.length === 0) {
|
|
33
|
+
this.build();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const queryTokens = tokenizer.tokenize(query.toLowerCase());
|
|
37
|
+
const scores = new Array(this._docMap.length).fill(0);
|
|
38
|
+
|
|
39
|
+
for (const token of queryTokens) {
|
|
40
|
+
this.tfidf.tfidfs(token, (i, measure) => {
|
|
41
|
+
if (i < scores.length) {
|
|
42
|
+
scores[i] += measure;
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const allDocs = store.getAll();
|
|
48
|
+
scores.forEach((_, i) => {
|
|
49
|
+
const doc = allDocs[i];
|
|
50
|
+
if (!doc) return;
|
|
51
|
+
const textLower = doc.content.toLowerCase();
|
|
52
|
+
for (const token of queryTokens) {
|
|
53
|
+
if (token.length > 3 && textLower.includes(token)) {
|
|
54
|
+
scores[i] += 0.5;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (doc.metadata) {
|
|
59
|
+
const metaText = JSON.stringify(doc.metadata).toLowerCase();
|
|
60
|
+
for (const token of queryTokens) {
|
|
61
|
+
if (token.length > 3 && metaText.includes(token)) {
|
|
62
|
+
scores[i] += 1.0;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
let results = scores
|
|
69
|
+
.map((score, i) => ({ score, doc: allDocs[i] }))
|
|
70
|
+
.filter(r => r.doc && r.score > 0);
|
|
71
|
+
|
|
72
|
+
if (filter.kind) {
|
|
73
|
+
results = results.filter(r => r.doc.kind === filter.kind);
|
|
74
|
+
}
|
|
75
|
+
if (filter.filename) {
|
|
76
|
+
results = results.filter(r =>
|
|
77
|
+
r.doc.filename.toLowerCase().includes(filter.filename.toLowerCase())
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
results.sort((a, b) => b.score - a.score);
|
|
82
|
+
|
|
83
|
+
const seenFiles = new Map();
|
|
84
|
+
const deduped = [];
|
|
85
|
+
for (const r of results) {
|
|
86
|
+
const fp = r.doc.filePath;
|
|
87
|
+
if (!seenFiles.has(fp)) {
|
|
88
|
+
seenFiles.set(fp, r);
|
|
89
|
+
deduped.push(r);
|
|
90
|
+
} else if (r.score > seenFiles.get(fp).score) {
|
|
91
|
+
const idx = deduped.findIndex(d => d.doc.filePath === fp);
|
|
92
|
+
deduped[idx] = r;
|
|
93
|
+
}
|
|
94
|
+
if (deduped.length >= topK) break;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return deduped.slice(0, topK).map(r => ({
|
|
98
|
+
...r.doc,
|
|
99
|
+
relevanceScore: parseFloat(r.score.toFixed(4)),
|
|
100
|
+
}));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
searchForErrors(errorType, topK = 6) {
|
|
104
|
+
const results = this.search(errorType, topK * 2);
|
|
105
|
+
return results
|
|
106
|
+
.map(doc => ({
|
|
107
|
+
...doc,
|
|
108
|
+
relevanceScore: doc.kind === 'log'
|
|
109
|
+
? doc.relevanceScore * 1.5
|
|
110
|
+
: doc.metadata?.isBugFix
|
|
111
|
+
? doc.relevanceScore * 1.3
|
|
112
|
+
: doc.relevanceScore,
|
|
113
|
+
}))
|
|
114
|
+
.sort((a, b) => b.relevanceScore - a.relevanceScore)
|
|
115
|
+
.slice(0, topK);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
searchForUsages(symbol, topK = 8) {
|
|
119
|
+
const query = `${symbol} usage import call reference`;
|
|
120
|
+
const results = this.search(query, topK * 2);
|
|
121
|
+
|
|
122
|
+
return results
|
|
123
|
+
.map(doc => {
|
|
124
|
+
let boost = 1;
|
|
125
|
+
if (doc.content.includes(symbol)) boost = 2;
|
|
126
|
+
if (doc.metadata?.imports?.some(i => i.includes(symbol))) boost = 2.5;
|
|
127
|
+
if (doc.metadata?.functions?.includes(symbol)) boost = 3;
|
|
128
|
+
return { ...doc, relevanceScore: doc.relevanceScore * boost };
|
|
129
|
+
})
|
|
130
|
+
.sort((a, b) => b.relevanceScore - a.relevanceScore)
|
|
131
|
+
.slice(0, topK);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
searchForImpact(target, topK = 8) {
|
|
135
|
+
const query = `${target} depends import module connection`;
|
|
136
|
+
const results = this.search(query, topK * 2);
|
|
137
|
+
|
|
138
|
+
return results
|
|
139
|
+
.map(doc => {
|
|
140
|
+
let boost = 1;
|
|
141
|
+
if (doc.content.includes(target)) boost = 2;
|
|
142
|
+
if (doc.metadata?.imports?.some(i => i.includes(target))) boost = 3;
|
|
143
|
+
return { ...doc, relevanceScore: doc.relevanceScore * boost };
|
|
144
|
+
})
|
|
145
|
+
.sort((a, b) => b.relevanceScore - a.relevanceScore)
|
|
146
|
+
.slice(0, topK);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
_docToText(doc) {
|
|
150
|
+
const parts = [
|
|
151
|
+
doc.filename,
|
|
152
|
+
doc.kind,
|
|
153
|
+
doc.content,
|
|
154
|
+
doc.metadata?.functions?.join(' ') || '',
|
|
155
|
+
doc.metadata?.classes?.join(' ') || '',
|
|
156
|
+
doc.metadata?.imports?.join(' ') || '',
|
|
157
|
+
doc.metadata?.exports?.join(' ') || '',
|
|
158
|
+
doc.metadata?.errors?.join(' ') || '',
|
|
159
|
+
doc.metadata?.patterns?.join(' ') || '',
|
|
160
|
+
doc.metadata?.tables?.join(' ') || '',
|
|
161
|
+
];
|
|
162
|
+
return parts.join(' ');
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
invalidate() {
|
|
166
|
+
this._built = false;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const indexer = new Indexer();
|
|
171
|
+
module.exports = indexer;
|