preflight-mcp 0.1.2 → 0.1.4
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 +49 -142
- package/README.zh-CN.md +141 -124
- package/dist/ast/treeSitter.js +588 -0
- package/dist/bundle/analysis.js +47 -0
- package/dist/bundle/context7.js +65 -36
- package/dist/bundle/facts.js +829 -0
- package/dist/bundle/github.js +34 -3
- package/dist/bundle/githubArchive.js +102 -29
- package/dist/bundle/overview.js +226 -48
- package/dist/bundle/service.js +250 -130
- package/dist/config.js +30 -3
- package/dist/context7/client.js +5 -2
- package/dist/evidence/dependencyGraph.js +1136 -0
- package/dist/http/server.js +109 -0
- package/dist/jobs/progressTracker.js +191 -0
- package/dist/search/sqliteFts.js +150 -10
- package/dist/server.js +340 -326
- package/dist/trace/service.js +108 -0
- package/dist/trace/store.js +170 -0
- package/package.json +4 -2
- package/dist/bundle/deepwiki.js +0 -206
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import http from 'node:http';
|
|
2
|
+
import { URL } from 'node:url';
|
|
3
|
+
import { logger } from '../logging/logger.js';
|
|
4
|
+
import { wrapPreflightError } from '../mcp/errorKinds.js';
|
|
5
|
+
import { generateDependencyGraph } from '../evidence/dependencyGraph.js';
|
|
6
|
+
import { traceQuery, traceUpsert } from '../trace/service.js';
|
|
7
|
+
function sendJson(res, status, body) {
|
|
8
|
+
const text = JSON.stringify(body, null, 2);
|
|
9
|
+
res.statusCode = status;
|
|
10
|
+
res.setHeader('content-type', 'application/json; charset=utf-8');
|
|
11
|
+
res.setHeader('cache-control', 'no-store');
|
|
12
|
+
res.end(text);
|
|
13
|
+
}
|
|
14
|
+
async function readJsonBody(req, maxBytes) {
|
|
15
|
+
const chunks = [];
|
|
16
|
+
let total = 0;
|
|
17
|
+
for await (const chunk of req) {
|
|
18
|
+
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
19
|
+
total += buf.length;
|
|
20
|
+
if (total > maxBytes) {
|
|
21
|
+
throw new Error(`Request body too large (>${maxBytes} bytes)`);
|
|
22
|
+
}
|
|
23
|
+
chunks.push(buf);
|
|
24
|
+
}
|
|
25
|
+
const raw = Buffer.concat(chunks).toString('utf8');
|
|
26
|
+
if (!raw.trim())
|
|
27
|
+
return {};
|
|
28
|
+
return JSON.parse(raw);
|
|
29
|
+
}
|
|
30
|
+
export function startHttpServer(cfg) {
|
|
31
|
+
if (!cfg.httpEnabled) {
|
|
32
|
+
logger.info('REST API disabled (PREFLIGHT_HTTP_ENABLED=false)');
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
const server = http.createServer(async (req, res) => {
|
|
36
|
+
try {
|
|
37
|
+
const method = (req.method ?? 'GET').toUpperCase();
|
|
38
|
+
const url = new URL(req.url ?? '/', `http://${cfg.httpHost}:${cfg.httpPort}`);
|
|
39
|
+
const pathname = url.pathname;
|
|
40
|
+
// Basic CORS (local development convenience). Keep permissive but local-only by default host.
|
|
41
|
+
res.setHeader('access-control-allow-origin', '*');
|
|
42
|
+
res.setHeader('access-control-allow-headers', 'content-type');
|
|
43
|
+
res.setHeader('access-control-allow-methods', 'GET,POST,OPTIONS');
|
|
44
|
+
if (method === 'OPTIONS') {
|
|
45
|
+
res.statusCode = 204;
|
|
46
|
+
res.end();
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
if (method === 'GET' && pathname === '/health') {
|
|
50
|
+
sendJson(res, 200, { ok: true, name: 'preflight-mcp', time: new Date().toISOString() });
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (method === 'POST' && pathname === '/api/v1/evidence/dependency-graph') {
|
|
54
|
+
const body = await readJsonBody(req, 2_000_000);
|
|
55
|
+
const out = await generateDependencyGraph(cfg, body);
|
|
56
|
+
sendJson(res, 200, out);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
if (method === 'POST' && pathname === '/api/v1/trace/upsert') {
|
|
60
|
+
const body = await readJsonBody(req, 2_000_000);
|
|
61
|
+
const out = await traceUpsert(cfg, body);
|
|
62
|
+
sendJson(res, 200, out);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (method === 'GET' && pathname === '/api/v1/trace') {
|
|
66
|
+
const query = Object.fromEntries(url.searchParams.entries());
|
|
67
|
+
// Convert known numeric params
|
|
68
|
+
const body = { ...query };
|
|
69
|
+
if (typeof query.limit === 'string')
|
|
70
|
+
body.limit = Number(query.limit);
|
|
71
|
+
if (typeof query.timeBudgetMs === 'string')
|
|
72
|
+
body.timeBudgetMs = Number(query.timeBudgetMs);
|
|
73
|
+
if (typeof query.maxBundles === 'string')
|
|
74
|
+
body.maxBundles = Number(query.maxBundles);
|
|
75
|
+
const out = await traceQuery(cfg, body);
|
|
76
|
+
sendJson(res, 200, out);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
sendJson(res, 404, { error: { message: `Not found: ${method} ${pathname}` } });
|
|
80
|
+
}
|
|
81
|
+
catch (err) {
|
|
82
|
+
const wrapped = wrapPreflightError(err);
|
|
83
|
+
sendJson(res, 400, { error: { message: wrapped.message } });
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
server.on('error', (err) => {
|
|
87
|
+
// Non-fatal: MCP should still work.
|
|
88
|
+
logger.warn(`REST server error: ${err instanceof Error ? err.message : String(err)}`);
|
|
89
|
+
});
|
|
90
|
+
try {
|
|
91
|
+
server.listen(cfg.httpPort, cfg.httpHost, () => {
|
|
92
|
+
logger.info(`REST API listening on http://${cfg.httpHost}:${cfg.httpPort}`);
|
|
93
|
+
});
|
|
94
|
+
// Allow process to exit if stdio transport closes.
|
|
95
|
+
server.unref();
|
|
96
|
+
return {
|
|
97
|
+
host: cfg.httpHost,
|
|
98
|
+
port: cfg.httpPort,
|
|
99
|
+
close: () => new Promise((resolve) => {
|
|
100
|
+
server.close(() => resolve());
|
|
101
|
+
}),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
catch (err) {
|
|
105
|
+
// Binding failures should not crash MCP.
|
|
106
|
+
logger.warn(`Failed to start REST API server: ${err instanceof Error ? err.message : String(err)}`);
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Progress tracking for long-running bundle creation tasks.
|
|
3
|
+
* Enables progress notifications via MCP and prevents duplicate task creation.
|
|
4
|
+
*/
|
|
5
|
+
import crypto from 'node:crypto';
|
|
6
|
+
function nowIso() {
|
|
7
|
+
return new Date().toISOString();
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Tracks progress of bundle creation tasks.
|
|
11
|
+
* Provides in-memory state for active tasks and emits progress updates.
|
|
12
|
+
*/
|
|
13
|
+
export class ProgressTracker {
|
|
14
|
+
tasks = new Map();
|
|
15
|
+
fingerprintToTaskId = new Map();
|
|
16
|
+
progressCallback;
|
|
17
|
+
/**
|
|
18
|
+
* Set a callback to receive progress updates.
|
|
19
|
+
* Used to forward updates to MCP progress notifications.
|
|
20
|
+
*/
|
|
21
|
+
setProgressCallback(callback) {
|
|
22
|
+
this.progressCallback = callback;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Start tracking a new task.
|
|
26
|
+
* @returns taskId for the new task
|
|
27
|
+
*/
|
|
28
|
+
startTask(fingerprint, repos) {
|
|
29
|
+
// Check if task already exists for this fingerprint
|
|
30
|
+
const existingTaskId = this.fingerprintToTaskId.get(fingerprint);
|
|
31
|
+
if (existingTaskId) {
|
|
32
|
+
const existingTask = this.tasks.get(existingTaskId);
|
|
33
|
+
if (existingTask && existingTask.phase !== 'complete' && existingTask.phase !== 'failed') {
|
|
34
|
+
// Return existing active task
|
|
35
|
+
return existingTaskId;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const taskId = crypto.randomUUID();
|
|
39
|
+
const now = nowIso();
|
|
40
|
+
const task = {
|
|
41
|
+
taskId,
|
|
42
|
+
fingerprint,
|
|
43
|
+
phase: 'starting',
|
|
44
|
+
progress: 0,
|
|
45
|
+
message: `Starting bundle creation for ${repos.join(', ')}`,
|
|
46
|
+
startedAt: now,
|
|
47
|
+
updatedAt: now,
|
|
48
|
+
repos,
|
|
49
|
+
};
|
|
50
|
+
this.tasks.set(taskId, task);
|
|
51
|
+
this.fingerprintToTaskId.set(fingerprint, taskId);
|
|
52
|
+
this.emitProgress(task);
|
|
53
|
+
return taskId;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Update progress for an existing task.
|
|
57
|
+
*/
|
|
58
|
+
updateProgress(taskId, phase, progress, message, total) {
|
|
59
|
+
const task = this.tasks.get(taskId);
|
|
60
|
+
if (!task)
|
|
61
|
+
return;
|
|
62
|
+
task.phase = phase;
|
|
63
|
+
task.progress = progress;
|
|
64
|
+
task.message = message;
|
|
65
|
+
task.updatedAt = nowIso();
|
|
66
|
+
if (total !== undefined) {
|
|
67
|
+
task.total = total;
|
|
68
|
+
}
|
|
69
|
+
this.emitProgress(task);
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Mark a task as complete.
|
|
73
|
+
*/
|
|
74
|
+
completeTask(taskId, bundleId) {
|
|
75
|
+
const task = this.tasks.get(taskId);
|
|
76
|
+
if (!task)
|
|
77
|
+
return;
|
|
78
|
+
task.phase = 'complete';
|
|
79
|
+
task.progress = 100;
|
|
80
|
+
task.message = `Bundle created: ${bundleId}`;
|
|
81
|
+
task.updatedAt = nowIso();
|
|
82
|
+
task.bundleId = bundleId;
|
|
83
|
+
this.emitProgress(task);
|
|
84
|
+
// Clean up after a delay to allow final status queries
|
|
85
|
+
setTimeout(() => {
|
|
86
|
+
this.tasks.delete(taskId);
|
|
87
|
+
if (this.fingerprintToTaskId.get(task.fingerprint) === taskId) {
|
|
88
|
+
this.fingerprintToTaskId.delete(task.fingerprint);
|
|
89
|
+
}
|
|
90
|
+
}, 60_000); // Keep completed task for 1 minute
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Mark a task as failed.
|
|
94
|
+
*/
|
|
95
|
+
failTask(taskId, error) {
|
|
96
|
+
const task = this.tasks.get(taskId);
|
|
97
|
+
if (!task)
|
|
98
|
+
return;
|
|
99
|
+
task.phase = 'failed';
|
|
100
|
+
task.message = `Failed: ${error}`;
|
|
101
|
+
task.updatedAt = nowIso();
|
|
102
|
+
task.error = error;
|
|
103
|
+
this.emitProgress(task);
|
|
104
|
+
// Clean up after a delay
|
|
105
|
+
setTimeout(() => {
|
|
106
|
+
this.tasks.delete(taskId);
|
|
107
|
+
if (this.fingerprintToTaskId.get(task.fingerprint) === taskId) {
|
|
108
|
+
this.fingerprintToTaskId.delete(task.fingerprint);
|
|
109
|
+
}
|
|
110
|
+
}, 60_000);
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Get task by ID.
|
|
114
|
+
*/
|
|
115
|
+
getTask(taskId) {
|
|
116
|
+
return this.tasks.get(taskId);
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Get task by fingerprint.
|
|
120
|
+
*/
|
|
121
|
+
getTaskByFingerprint(fingerprint) {
|
|
122
|
+
const taskId = this.fingerprintToTaskId.get(fingerprint);
|
|
123
|
+
if (!taskId)
|
|
124
|
+
return undefined;
|
|
125
|
+
return this.tasks.get(taskId);
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* List all active (non-complete, non-failed) tasks.
|
|
129
|
+
*/
|
|
130
|
+
listActiveTasks() {
|
|
131
|
+
return Array.from(this.tasks.values()).filter((t) => t.phase !== 'complete' && t.phase !== 'failed');
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* List all tasks (including recently completed/failed).
|
|
135
|
+
*/
|
|
136
|
+
listAllTasks() {
|
|
137
|
+
return Array.from(this.tasks.values());
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Check if a task is active (in progress).
|
|
141
|
+
*/
|
|
142
|
+
isTaskActive(fingerprint) {
|
|
143
|
+
const task = this.getTaskByFingerprint(fingerprint);
|
|
144
|
+
return task !== undefined && task.phase !== 'complete' && task.phase !== 'failed';
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Remove a task (e.g., when lock times out).
|
|
148
|
+
*/
|
|
149
|
+
removeTask(taskId) {
|
|
150
|
+
const task = this.tasks.get(taskId);
|
|
151
|
+
if (task) {
|
|
152
|
+
this.tasks.delete(taskId);
|
|
153
|
+
if (this.fingerprintToTaskId.get(task.fingerprint) === taskId) {
|
|
154
|
+
this.fingerprintToTaskId.delete(task.fingerprint);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
emitProgress(task) {
|
|
159
|
+
if (this.progressCallback) {
|
|
160
|
+
this.progressCallback(task);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
// Singleton instance for global access
|
|
165
|
+
let globalTracker;
|
|
166
|
+
export function getProgressTracker() {
|
|
167
|
+
if (!globalTracker) {
|
|
168
|
+
globalTracker = new ProgressTracker();
|
|
169
|
+
}
|
|
170
|
+
return globalTracker;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Helper to format bytes for display.
|
|
174
|
+
*/
|
|
175
|
+
export function formatBytes(bytes) {
|
|
176
|
+
if (bytes < 1024)
|
|
177
|
+
return `${bytes}B`;
|
|
178
|
+
if (bytes < 1024 * 1024)
|
|
179
|
+
return `${(bytes / 1024).toFixed(1)}KB`;
|
|
180
|
+
if (bytes < 1024 * 1024 * 1024)
|
|
181
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
182
|
+
return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)}GB`;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Helper to calculate percentage.
|
|
186
|
+
*/
|
|
187
|
+
export function calcPercent(current, total) {
|
|
188
|
+
if (total <= 0)
|
|
189
|
+
return 0;
|
|
190
|
+
return Math.min(100, Math.round((current / total) * 100));
|
|
191
|
+
}
|
package/dist/search/sqliteFts.js
CHANGED
|
@@ -278,6 +278,118 @@ export async function rebuildIndex(dbPathOrFiles, filesOrDbPath, opts) {
|
|
|
278
278
|
db.close();
|
|
279
279
|
}
|
|
280
280
|
}
|
|
281
|
+
/**
|
|
282
|
+
* Extract code context for a search hit.
|
|
283
|
+
* Finds the surrounding function/class definition and surrounding lines.
|
|
284
|
+
*/
|
|
285
|
+
function extractContext(fileContent, hitLineNo) {
|
|
286
|
+
const lines = fileContent.split('\n');
|
|
287
|
+
const hitIndex = hitLineNo - 1; // Convert 1-based to 0-based
|
|
288
|
+
if (hitIndex < 0 || hitIndex >= lines.length) {
|
|
289
|
+
return undefined;
|
|
290
|
+
}
|
|
291
|
+
// Extract surrounding lines (±3 lines)
|
|
292
|
+
const surroundStart = Math.max(0, hitIndex - 3);
|
|
293
|
+
const surroundEnd = Math.min(lines.length - 1, hitIndex + 3);
|
|
294
|
+
const surroundingLines = lines.slice(surroundStart, surroundEnd + 1);
|
|
295
|
+
// Find function/class definition by scanning upwards (max 50 lines)
|
|
296
|
+
let functionName;
|
|
297
|
+
let className;
|
|
298
|
+
let startLine = hitLineNo;
|
|
299
|
+
let endLine = hitLineNo;
|
|
300
|
+
const scanStartIndex = Math.max(0, hitIndex - 50);
|
|
301
|
+
// Patterns for TypeScript/JavaScript/Python/Go functions and classes
|
|
302
|
+
const functionPatterns = [
|
|
303
|
+
// TypeScript/JavaScript
|
|
304
|
+
/^\s*(?:export\s+)?(?:async\s+)?function\s+([a-zA-Z_$][\w$]*)\s*\(/,
|
|
305
|
+
/^\s*(?:export\s+)?(?:const|let|var)\s+([a-zA-Z_$][\w$]*)\s*=\s*(?:async\s+)?\(/,
|
|
306
|
+
/^\s*(?:async\s+)?([a-zA-Z_$][\w$]*)\s*\([^)]*\)\s*\{/,
|
|
307
|
+
/^\s*([a-zA-Z_$][\w$]*)\s*:\s*(?:async\s+)?function\s*\(/,
|
|
308
|
+
// Python
|
|
309
|
+
/^\s*(?:async\s+)?def\s+([a-zA-Z_][\w]*)\s*\(/,
|
|
310
|
+
// Go
|
|
311
|
+
/^\s*func\s+(?:\([^)]*\)\s*)?([a-zA-Z_][\w]*)\s*\(/,
|
|
312
|
+
];
|
|
313
|
+
const classPatterns = [
|
|
314
|
+
// TypeScript/JavaScript
|
|
315
|
+
/^\s*(?:export\s+)?(?:abstract\s+)?class\s+([a-zA-Z_$][\w$]*)/,
|
|
316
|
+
/^\s*(?:export\s+)?interface\s+([a-zA-Z_$][\w$]*)/,
|
|
317
|
+
/^\s*(?:export\s+)?type\s+([a-zA-Z_$][\w$]*)\s*=/,
|
|
318
|
+
// Python
|
|
319
|
+
/^\s*class\s+([a-zA-Z_][\w]*)\s*[:(]/,
|
|
320
|
+
// Go
|
|
321
|
+
/^\s*type\s+([a-zA-Z_][\w]*)\s+struct/,
|
|
322
|
+
];
|
|
323
|
+
// Scan upward to find function or class definition
|
|
324
|
+
for (let i = hitIndex; i >= scanStartIndex; i--) {
|
|
325
|
+
const line = lines[i] ?? '';
|
|
326
|
+
// Try to match function patterns
|
|
327
|
+
if (!functionName) {
|
|
328
|
+
for (const pattern of functionPatterns) {
|
|
329
|
+
const match = line.match(pattern);
|
|
330
|
+
if (match?.[1]) {
|
|
331
|
+
functionName = match[1];
|
|
332
|
+
startLine = i + 1; // Convert to 1-based
|
|
333
|
+
break;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
// Try to match class patterns (only if we haven't found function yet)
|
|
338
|
+
if (!className) {
|
|
339
|
+
for (const pattern of classPatterns) {
|
|
340
|
+
const match = line.match(pattern);
|
|
341
|
+
if (match?.[1]) {
|
|
342
|
+
className = match[1];
|
|
343
|
+
if (!functionName) {
|
|
344
|
+
startLine = i + 1;
|
|
345
|
+
}
|
|
346
|
+
break;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
// If we found function name, stop scanning
|
|
351
|
+
if (functionName) {
|
|
352
|
+
break;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
// Find end of the function/block by scanning downward for closing brace
|
|
356
|
+
// Simple bracket matching (stops at first balanced closing brace)
|
|
357
|
+
if (functionName || className) {
|
|
358
|
+
let braceCount = 0;
|
|
359
|
+
let foundOpenBrace = false;
|
|
360
|
+
for (let i = startLine - 1; i < lines.length && i < hitIndex + 100; i++) {
|
|
361
|
+
const line = lines[i] ?? '';
|
|
362
|
+
for (const char of line) {
|
|
363
|
+
if (char === '{') {
|
|
364
|
+
braceCount++;
|
|
365
|
+
foundOpenBrace = true;
|
|
366
|
+
}
|
|
367
|
+
else if (char === '}') {
|
|
368
|
+
braceCount--;
|
|
369
|
+
if (foundOpenBrace && braceCount === 0) {
|
|
370
|
+
endLine = i + 1;
|
|
371
|
+
return {
|
|
372
|
+
functionName,
|
|
373
|
+
className,
|
|
374
|
+
startLine,
|
|
375
|
+
endLine,
|
|
376
|
+
surroundingLines,
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
// If we didn't find closing brace, estimate end line
|
|
383
|
+
endLine = Math.min(lines.length, startLine + 50);
|
|
384
|
+
}
|
|
385
|
+
return {
|
|
386
|
+
functionName,
|
|
387
|
+
className,
|
|
388
|
+
startLine,
|
|
389
|
+
endLine,
|
|
390
|
+
surroundingLines,
|
|
391
|
+
};
|
|
392
|
+
}
|
|
281
393
|
function tokenizeForSafeQuery(input) {
|
|
282
394
|
const s = input.trim();
|
|
283
395
|
if (!s)
|
|
@@ -308,7 +420,7 @@ export function buildFtsQuery(input) {
|
|
|
308
420
|
// Quote each token to keep syntax safe.
|
|
309
421
|
return tokens.map((t) => `"${t.replaceAll('"', '""')}"`).join(' OR ');
|
|
310
422
|
}
|
|
311
|
-
export function searchIndex(dbPath, query, scope, limit) {
|
|
423
|
+
export function searchIndex(dbPath, query, scope, limit, bundleRoot) {
|
|
312
424
|
const db = new Database(dbPath, { readonly: true });
|
|
313
425
|
try {
|
|
314
426
|
const ftsQuery = buildFtsQuery(query);
|
|
@@ -326,13 +438,41 @@ export function searchIndex(dbPath, query, scope, limit) {
|
|
|
326
438
|
LIMIT ?
|
|
327
439
|
`);
|
|
328
440
|
const rows = stmt.all(ftsQuery, limit);
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
441
|
+
// Cache for file contents to avoid re-reading same files
|
|
442
|
+
const fileCache = new Map();
|
|
443
|
+
return rows.map((r) => {
|
|
444
|
+
const hit = {
|
|
445
|
+
path: r.path,
|
|
446
|
+
repo: r.repo,
|
|
447
|
+
kind: r.kind,
|
|
448
|
+
lineNo: r.lineNo,
|
|
449
|
+
snippet: r.snippet,
|
|
450
|
+
};
|
|
451
|
+
// Add context for code files if bundleRoot is provided
|
|
452
|
+
if (r.kind === 'code' && bundleRoot) {
|
|
453
|
+
try {
|
|
454
|
+
// r.path is bundleNormRelativePath (e.g., "repos/owner/repo/norm/path/to/file.ts")
|
|
455
|
+
// Construct absolute path to the normalized file
|
|
456
|
+
const filePath = path.join(bundleRoot, r.path);
|
|
457
|
+
// Read file content (use cache to avoid re-reading)
|
|
458
|
+
let fileContent = fileCache.get(filePath);
|
|
459
|
+
if (!fileContent) {
|
|
460
|
+
fileContent = fsSync.readFileSync(filePath, 'utf8');
|
|
461
|
+
fileCache.set(filePath, fileContent);
|
|
462
|
+
}
|
|
463
|
+
// Extract context
|
|
464
|
+
const context = extractContext(fileContent, r.lineNo);
|
|
465
|
+
if (context) {
|
|
466
|
+
hit.context = context;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
catch (err) {
|
|
470
|
+
// Silently skip context extraction on error (file not found, etc.)
|
|
471
|
+
// Context is optional enhancement, shouldn't break search
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
return hit;
|
|
475
|
+
});
|
|
336
476
|
}
|
|
337
477
|
finally {
|
|
338
478
|
db.close();
|
|
@@ -464,9 +604,9 @@ function generateVerificationSummary(claim, supporting, contradicting, related,
|
|
|
464
604
|
* 2. Calculating an overall confidence score
|
|
465
605
|
* 3. Providing a human-readable summary
|
|
466
606
|
*/
|
|
467
|
-
export function verifyClaimInIndex(dbPath, claim, scope, limit) {
|
|
607
|
+
export function verifyClaimInIndex(dbPath, claim, scope, limit, bundleRoot) {
|
|
468
608
|
// Get raw search results
|
|
469
|
-
const rawHits = searchIndex(dbPath, claim, scope, limit);
|
|
609
|
+
const rawHits = searchIndex(dbPath, claim, scope, limit, bundleRoot);
|
|
470
610
|
// Extract tokens from claim for classification
|
|
471
611
|
const claimTokens = tokenizeForSafeQuery(claim);
|
|
472
612
|
// Classify each hit
|