brainbank 0.1.0-beta.1
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/LICENSE +21 -0
- package/README.md +155 -0
- package/assets/architecture.png +0 -0
- package/bin/brainbank +18 -0
- package/bin/brainbank-mcp +19 -0
- package/dist/chunk-3YBCD6DI.js +117 -0
- package/dist/chunk-3YBCD6DI.js.map +1 -0
- package/dist/chunk-63GBCDS5.js +3249 -0
- package/dist/chunk-63GBCDS5.js.map +1 -0
- package/dist/chunk-DMFMTOHF.js +123 -0
- package/dist/chunk-DMFMTOHF.js.map +1 -0
- package/dist/chunk-FQYKWB2Q.js +136 -0
- package/dist/chunk-FQYKWB2Q.js.map +1 -0
- package/dist/chunk-IMJJ2VEM.js +74 -0
- package/dist/chunk-IMJJ2VEM.js.map +1 -0
- package/dist/chunk-M744PCJQ.js +43 -0
- package/dist/chunk-M744PCJQ.js.map +1 -0
- package/dist/chunk-O3J6ZIXK.js +82 -0
- package/dist/chunk-O3J6ZIXK.js.map +1 -0
- package/dist/chunk-OPH7GZ7U.js +124 -0
- package/dist/chunk-OPH7GZ7U.js.map +1 -0
- package/dist/chunk-PXEWQMN7.js +89 -0
- package/dist/chunk-PXEWQMN7.js.map +1 -0
- package/dist/chunk-RDQYDLYZ.js +69 -0
- package/dist/chunk-RDQYDLYZ.js.map +1 -0
- package/dist/chunk-VIIHPCC4.js +254 -0
- package/dist/chunk-VIIHPCC4.js.map +1 -0
- package/dist/chunk-WCQVDF3K.js +14 -0
- package/dist/chunk-WCQVDF3K.js.map +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +3076 -0
- package/dist/cli.js.map +1 -0
- package/dist/haiku-expander-YRSIPGKP.js +8 -0
- package/dist/haiku-expander-YRSIPGKP.js.map +1 -0
- package/dist/haiku-pruner-SHAXUPY6.js +8 -0
- package/dist/haiku-pruner-SHAXUPY6.js.map +1 -0
- package/dist/http-server-QUXHLWUM.js +9 -0
- package/dist/http-server-QUXHLWUM.js.map +1 -0
- package/dist/index.d.ts +2161 -0
- package/dist/index.js +357 -0
- package/dist/index.js.map +1 -0
- package/dist/local-embedding-NZQTILGV.js +8 -0
- package/dist/local-embedding-NZQTILGV.js.map +1 -0
- package/dist/mcp.d.ts +2 -0
- package/dist/mcp.js +334 -0
- package/dist/mcp.js.map +1 -0
- package/dist/openai-embedding-ZP5TSUJG.js +8 -0
- package/dist/openai-embedding-ZP5TSUJG.js.map +1 -0
- package/dist/perplexity-context-embedding-GI5PHE6X.js +9 -0
- package/dist/perplexity-context-embedding-GI5PHE6X.js.map +1 -0
- package/dist/perplexity-embedding-KZRYGJRC.js +10 -0
- package/dist/perplexity-embedding-KZRYGJRC.js.map +1 -0
- package/dist/plugin-IKQ6IRSJ.js +32 -0
- package/dist/plugin-IKQ6IRSJ.js.map +1 -0
- package/dist/resolve-ASGLBNUC.js +10 -0
- package/dist/resolve-ASGLBNUC.js.map +1 -0
- package/dist/stats-tui-ZY2NQSEA.js +1904 -0
- package/dist/stats-tui-ZY2NQSEA.js.map +1 -0
- package/package.json +96 -0
- package/src/brainbank.ts +617 -0
- package/src/cli/commands/collection.ts +77 -0
- package/src/cli/commands/context.ts +179 -0
- package/src/cli/commands/daemon.ts +100 -0
- package/src/cli/commands/docs.ts +71 -0
- package/src/cli/commands/files.ts +69 -0
- package/src/cli/commands/help.ts +77 -0
- package/src/cli/commands/index.ts +482 -0
- package/src/cli/commands/kv.ts +140 -0
- package/src/cli/commands/mcp-export.ts +273 -0
- package/src/cli/commands/mcp.ts +6 -0
- package/src/cli/commands/reembed.ts +30 -0
- package/src/cli/commands/scan.ts +336 -0
- package/src/cli/commands/search.ts +203 -0
- package/src/cli/commands/stats.ts +68 -0
- package/src/cli/commands/status.ts +47 -0
- package/src/cli/commands/watch.ts +47 -0
- package/src/cli/factory/brain-context.ts +43 -0
- package/src/cli/factory/builtin-registration.ts +87 -0
- package/src/cli/factory/config-loader.ts +77 -0
- package/src/cli/factory/index.ts +69 -0
- package/src/cli/factory/plugin-loader.ts +325 -0
- package/src/cli/index.ts +71 -0
- package/src/cli/server-client.ts +178 -0
- package/src/cli/tui/index-tui.tsx +667 -0
- package/src/cli/tui/stats-data.ts +523 -0
- package/src/cli/tui/stats-search.ts +262 -0
- package/src/cli/tui/stats-tui.tsx +1465 -0
- package/src/cli/tui/tree-scanner.ts +650 -0
- package/src/cli/utils.ts +137 -0
- package/src/config.ts +49 -0
- package/src/constants.ts +21 -0
- package/src/db/adapter.ts +112 -0
- package/src/db/metadata.ts +130 -0
- package/src/db/migrations.ts +66 -0
- package/src/db/sqlite-adapter.ts +218 -0
- package/src/db/tracker.ts +91 -0
- package/src/engine/index-api.ts +81 -0
- package/src/engine/reembed.ts +206 -0
- package/src/engine/search-api.ts +218 -0
- package/src/index.ts +154 -0
- package/src/lib/fts.ts +57 -0
- package/src/lib/languages.ts +180 -0
- package/src/lib/logger.ts +126 -0
- package/src/lib/math.ts +87 -0
- package/src/lib/provider-key.ts +20 -0
- package/src/lib/prune.ts +71 -0
- package/src/lib/rrf.ts +133 -0
- package/src/lib/write-lock.ts +108 -0
- package/src/mcp/mcp-server.ts +195 -0
- package/src/mcp/workspace-factory.ts +68 -0
- package/src/mcp/workspace-pool.ts +224 -0
- package/src/plugin.ts +381 -0
- package/src/providers/embeddings/embedding-worker-thread.ts +95 -0
- package/src/providers/embeddings/embedding-worker.ts +141 -0
- package/src/providers/embeddings/local-embedding.ts +115 -0
- package/src/providers/embeddings/openai-embedding.ts +167 -0
- package/src/providers/embeddings/perplexity-context-embedding.ts +195 -0
- package/src/providers/embeddings/perplexity-embedding.ts +165 -0
- package/src/providers/embeddings/resolve.ts +34 -0
- package/src/providers/pruners/haiku-expander.ts +166 -0
- package/src/providers/pruners/haiku-pruner.ts +112 -0
- package/src/providers/vector/hnsw-index.ts +174 -0
- package/src/providers/vector/hnsw-loader.ts +129 -0
- package/src/search/bm25-boost.ts +69 -0
- package/src/search/context-builder.ts +251 -0
- package/src/search/keyword/composite-bm25-search.ts +47 -0
- package/src/search/types.ts +37 -0
- package/src/search/vector/composite-vector-search.ts +61 -0
- package/src/search/vector/mmr.ts +64 -0
- package/src/services/collection.ts +384 -0
- package/src/services/daemon.ts +87 -0
- package/src/services/http-server.ts +336 -0
- package/src/services/kv-service.ts +64 -0
- package/src/services/plugin-registry.ts +77 -0
- package/src/services/watch.ts +340 -0
- package/src/services/webhook-server.ts +100 -0
- package/src/types.ts +493 -0
|
@@ -0,0 +1,523 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* stats-data.ts — Pure data-fetching functions for the Stats TUI.
|
|
3
|
+
*
|
|
4
|
+
* Opens a read-only `node:sqlite` DatabaseSync connection and queries
|
|
5
|
+
* code_chunks, code_imports, code_call_edges, code_symbols, embedding_meta.
|
|
6
|
+
* Zero state, zero React — just data.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
10
|
+
import * as fs from 'node:fs';
|
|
11
|
+
import * as path from 'node:path';
|
|
12
|
+
|
|
13
|
+
// ── Types ─────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
export interface StatsOverview {
|
|
16
|
+
files: number;
|
|
17
|
+
chunks: number;
|
|
18
|
+
symbols: number;
|
|
19
|
+
callEdges: number;
|
|
20
|
+
importEdges: number;
|
|
21
|
+
hnswSize: number;
|
|
22
|
+
dbSizeMB: number;
|
|
23
|
+
embeddingModel: string;
|
|
24
|
+
repoPath: string;
|
|
25
|
+
plugins: string[];
|
|
26
|
+
pruner: string;
|
|
27
|
+
expander: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface LanguageStat {
|
|
31
|
+
language: string;
|
|
32
|
+
chunks: number;
|
|
33
|
+
files: number;
|
|
34
|
+
percent: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface DirectoryStat {
|
|
38
|
+
dir: string;
|
|
39
|
+
files: number;
|
|
40
|
+
chunks: number;
|
|
41
|
+
percent: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface FileStat {
|
|
45
|
+
filePath: string;
|
|
46
|
+
fileName: string;
|
|
47
|
+
language: string;
|
|
48
|
+
chunks: number;
|
|
49
|
+
symbols: number;
|
|
50
|
+
startLine: number;
|
|
51
|
+
endLine: number;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface FileDetailInfo {
|
|
55
|
+
filePath: string;
|
|
56
|
+
language: string;
|
|
57
|
+
chunks: number;
|
|
58
|
+
symbols: SymbolInfo[];
|
|
59
|
+
importsOut: string[];
|
|
60
|
+
importsIn: string[];
|
|
61
|
+
callEdgesOut: number;
|
|
62
|
+
callEdgesIn: number;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface SymbolInfo {
|
|
66
|
+
name: string;
|
|
67
|
+
kind: string;
|
|
68
|
+
line: number;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface ChunkInfo {
|
|
72
|
+
id: number;
|
|
73
|
+
chunkType: string;
|
|
74
|
+
name: string | null;
|
|
75
|
+
startLine: number;
|
|
76
|
+
endLine: number;
|
|
77
|
+
content: string;
|
|
78
|
+
language: string;
|
|
79
|
+
callsOut: string[];
|
|
80
|
+
calledBy: string[];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface CallTreeNode {
|
|
84
|
+
chunkId: number;
|
|
85
|
+
symbol: string;
|
|
86
|
+
filePath: string;
|
|
87
|
+
children: CallTreeNode[];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
// ── Data Access ───────────────────────────────────
|
|
92
|
+
|
|
93
|
+
/** Open a read-only node:sqlite connection. */
|
|
94
|
+
function openDb(dbPath: string): DatabaseSync {
|
|
95
|
+
return new DatabaseSync(dbPath, { readOnly: true } as ConstructorParameters<typeof DatabaseSync>[1]);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Check if a table exists. */
|
|
99
|
+
function tableExists(db: DatabaseSync, name: string): boolean {
|
|
100
|
+
const row = db.prepare(
|
|
101
|
+
`SELECT 1 as found FROM sqlite_master WHERE type='table' AND name=?`
|
|
102
|
+
).get(name) as Record<string, unknown> | undefined;
|
|
103
|
+
return !!row;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Safe count query. */
|
|
107
|
+
function countQuery(db: DatabaseSync, sql: string, ...params: (string | number | bigint | null | Uint8Array)[]): number {
|
|
108
|
+
const row = db.prepare(sql).get(...params) as { c: number } | undefined;
|
|
109
|
+
return row?.c ?? 0;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
// ── Public API ────────────────────────────────────
|
|
114
|
+
|
|
115
|
+
export function fetchOverview(dbPath: string, repoPath: string, configPath: string): StatsOverview {
|
|
116
|
+
const db = openDb(dbPath);
|
|
117
|
+
try {
|
|
118
|
+
const hasChunks = tableExists(db, 'code_chunks');
|
|
119
|
+
|
|
120
|
+
const files = hasChunks ? countQuery(db, 'SELECT COUNT(DISTINCT file_path) as c FROM code_chunks') : 0;
|
|
121
|
+
const chunks = hasChunks ? countQuery(db, 'SELECT COUNT(*) as c FROM code_chunks') : 0;
|
|
122
|
+
const symbols = hasChunks ? countQuery(db, "SELECT COUNT(*) as c FROM code_chunks WHERE name IS NOT NULL AND name != ''") : 0;
|
|
123
|
+
const callEdges = tableExists(db, 'code_call_edges') ? countQuery(db, 'SELECT COUNT(*) as c FROM code_call_edges') : 0;
|
|
124
|
+
const importEdges = tableExists(db, 'code_imports') ? countQuery(db, 'SELECT COUNT(*) as c FROM code_imports') : 0;
|
|
125
|
+
const hnswSize = tableExists(db, 'code_vectors') ? countQuery(db, 'SELECT COUNT(*) as c FROM code_vectors') : 0;
|
|
126
|
+
|
|
127
|
+
// DB file size
|
|
128
|
+
const stat = fs.statSync(dbPath);
|
|
129
|
+
const dbSizeMB = Math.round(stat.size / 1048576 * 10) / 10;
|
|
130
|
+
|
|
131
|
+
// Embedding model
|
|
132
|
+
let embeddingModel = 'unknown';
|
|
133
|
+
if (tableExists(db, 'embedding_meta')) {
|
|
134
|
+
for (const key of ['provider_key', 'provider', 'model']) {
|
|
135
|
+
const row = db.prepare(`SELECT value FROM embedding_meta WHERE key = ?`).get(key) as { value: string } | undefined;
|
|
136
|
+
if (row) { embeddingModel = row.value; break; }
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Config
|
|
141
|
+
let plugins: string[] = ['code'];
|
|
142
|
+
let pruner = 'none';
|
|
143
|
+
let expander = 'none';
|
|
144
|
+
try {
|
|
145
|
+
const raw = fs.readFileSync(configPath, 'utf-8');
|
|
146
|
+
const config = JSON.parse(raw) as Record<string, unknown>;
|
|
147
|
+
if (Array.isArray(config.plugins)) plugins = config.plugins as string[];
|
|
148
|
+
if (typeof config.pruner === 'string') pruner = config.pruner;
|
|
149
|
+
if (typeof config.expander === 'string') expander = config.expander;
|
|
150
|
+
} catch { /* no config */ }
|
|
151
|
+
|
|
152
|
+
return { files, chunks, symbols, callEdges, importEdges, hnswSize, dbSizeMB, embeddingModel, repoPath, plugins, pruner, expander };
|
|
153
|
+
} finally {
|
|
154
|
+
db.close();
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function fetchLanguageBreakdown(dbPath: string): LanguageStat[] {
|
|
159
|
+
const db = openDb(dbPath);
|
|
160
|
+
try {
|
|
161
|
+
if (!tableExists(db, 'code_chunks')) return [];
|
|
162
|
+
|
|
163
|
+
const rows = db.prepare(`
|
|
164
|
+
SELECT language, COUNT(*) as chunks, COUNT(DISTINCT file_path) as files
|
|
165
|
+
FROM code_chunks
|
|
166
|
+
GROUP BY language
|
|
167
|
+
ORDER BY chunks DESC
|
|
168
|
+
`).all() as { language: string; chunks: number; files: number }[];
|
|
169
|
+
|
|
170
|
+
const total = rows.reduce((sum, r) => sum + r.chunks, 0);
|
|
171
|
+
return rows.map(r => ({
|
|
172
|
+
language: r.language,
|
|
173
|
+
chunks: r.chunks,
|
|
174
|
+
files: r.files,
|
|
175
|
+
percent: total > 0 ? Math.round(r.chunks / total * 1000) / 10 : 0,
|
|
176
|
+
}));
|
|
177
|
+
} finally {
|
|
178
|
+
db.close();
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function fetchDirectories(dbPath: string): DirectoryStat[] {
|
|
183
|
+
const db = openDb(dbPath);
|
|
184
|
+
try {
|
|
185
|
+
if (!tableExists(db, 'code_chunks')) return [];
|
|
186
|
+
|
|
187
|
+
const rows = db.prepare(`
|
|
188
|
+
SELECT
|
|
189
|
+
CASE
|
|
190
|
+
WHEN INSTR(file_path, '/') > 0 THEN SUBSTR(file_path, 1, INSTR(file_path, '/') - 1)
|
|
191
|
+
ELSE file_path
|
|
192
|
+
END as dir,
|
|
193
|
+
COUNT(DISTINCT file_path) as files,
|
|
194
|
+
COUNT(*) as chunks
|
|
195
|
+
FROM code_chunks
|
|
196
|
+
GROUP BY dir
|
|
197
|
+
ORDER BY chunks DESC
|
|
198
|
+
`).all() as { dir: string; files: number; chunks: number }[];
|
|
199
|
+
|
|
200
|
+
const total = rows.reduce((sum, r) => sum + r.chunks, 0);
|
|
201
|
+
return rows.map(r => ({
|
|
202
|
+
dir: r.dir,
|
|
203
|
+
files: r.files,
|
|
204
|
+
chunks: r.chunks,
|
|
205
|
+
percent: total > 0 ? Math.round(r.chunks / total * 1000) / 10 : 0,
|
|
206
|
+
}));
|
|
207
|
+
} finally {
|
|
208
|
+
db.close();
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export function fetchFilesInDir(dbPath: string, dir: string): FileStat[] {
|
|
213
|
+
const db = openDb(dbPath);
|
|
214
|
+
try {
|
|
215
|
+
if (!tableExists(db, 'code_chunks')) return [];
|
|
216
|
+
|
|
217
|
+
const rows = db.prepare(`
|
|
218
|
+
SELECT
|
|
219
|
+
file_path,
|
|
220
|
+
language,
|
|
221
|
+
COUNT(*) as chunks,
|
|
222
|
+
COUNT(CASE WHEN name IS NOT NULL AND name != '' THEN 1 END) as symbols,
|
|
223
|
+
MIN(start_line) as min_line,
|
|
224
|
+
MAX(end_line) as max_line
|
|
225
|
+
FROM code_chunks
|
|
226
|
+
WHERE file_path LIKE ? || '%'
|
|
227
|
+
GROUP BY file_path
|
|
228
|
+
ORDER BY chunks DESC, file_path
|
|
229
|
+
`).all(`${dir}/`) as { file_path: string; language: string; chunks: number; symbols: number; min_line: number; max_line: number }[];
|
|
230
|
+
|
|
231
|
+
return rows.map(r => ({
|
|
232
|
+
filePath: r.file_path,
|
|
233
|
+
fileName: path.basename(r.file_path),
|
|
234
|
+
language: r.language,
|
|
235
|
+
chunks: r.chunks,
|
|
236
|
+
symbols: r.symbols,
|
|
237
|
+
startLine: r.min_line,
|
|
238
|
+
endLine: r.max_line,
|
|
239
|
+
}));
|
|
240
|
+
} finally {
|
|
241
|
+
db.close();
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export function fetchFileDetail(dbPath: string, filePath: string): FileDetailInfo {
|
|
246
|
+
const db = openDb(dbPath);
|
|
247
|
+
try {
|
|
248
|
+
// Language + chunk count
|
|
249
|
+
const meta = db.prepare(`
|
|
250
|
+
SELECT language, COUNT(*) as chunks
|
|
251
|
+
FROM code_chunks WHERE file_path = ?
|
|
252
|
+
`).get(filePath) as { language: string; chunks: number } | undefined;
|
|
253
|
+
|
|
254
|
+
// Symbols — extract named chunks as symbols
|
|
255
|
+
const symbols = db.prepare(`
|
|
256
|
+
SELECT name, chunk_type as kind, start_line as line
|
|
257
|
+
FROM code_chunks
|
|
258
|
+
WHERE file_path = ? AND name IS NOT NULL AND name != ''
|
|
259
|
+
ORDER BY start_line
|
|
260
|
+
`).all(filePath) as unknown as SymbolInfo[];
|
|
261
|
+
|
|
262
|
+
// Imports out (this file imports...)
|
|
263
|
+
let importsOut: string[] = [];
|
|
264
|
+
if (tableExists(db, 'code_imports')) {
|
|
265
|
+
importsOut = (db.prepare(`
|
|
266
|
+
SELECT imports_path FROM code_imports WHERE file_path = ?
|
|
267
|
+
`).all(filePath) as { imports_path: string }[]).map(r => r.imports_path);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Imports in (who imports this file)
|
|
271
|
+
let importsIn: string[] = [];
|
|
272
|
+
if (tableExists(db, 'code_imports')) {
|
|
273
|
+
const base = path.basename(filePath, path.extname(filePath));
|
|
274
|
+
importsIn = (db.prepare(`
|
|
275
|
+
SELECT file_path FROM code_imports WHERE imports_path LIKE ?
|
|
276
|
+
`).all(`%${base}%`) as { file_path: string }[]).map(r => r.file_path);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Call edges
|
|
280
|
+
let callEdgesOut = 0;
|
|
281
|
+
let callEdgesIn = 0;
|
|
282
|
+
if (tableExists(db, 'code_call_edges')) {
|
|
283
|
+
const chunkIds = (db.prepare(
|
|
284
|
+
`SELECT id FROM code_chunks WHERE file_path = ?`
|
|
285
|
+
).all(filePath) as { id: number }[]).map(r => r.id);
|
|
286
|
+
|
|
287
|
+
if (chunkIds.length > 0) {
|
|
288
|
+
const ph = chunkIds.map(() => '?').join(',');
|
|
289
|
+
callEdgesOut = countQuery(db, `SELECT COUNT(*) as c FROM code_call_edges WHERE caller_chunk_id IN (${ph})`, ...chunkIds);
|
|
290
|
+
callEdgesIn = countQuery(db, `SELECT COUNT(*) as c FROM code_call_edges WHERE callee_chunk_id IN (${ph})`, ...chunkIds);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return {
|
|
295
|
+
filePath,
|
|
296
|
+
language: meta?.language ?? 'unknown',
|
|
297
|
+
chunks: meta?.chunks ?? 0,
|
|
298
|
+
symbols,
|
|
299
|
+
importsOut,
|
|
300
|
+
importsIn,
|
|
301
|
+
callEdgesOut,
|
|
302
|
+
callEdgesIn,
|
|
303
|
+
};
|
|
304
|
+
} finally {
|
|
305
|
+
db.close();
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
export function fetchChunksForFile(dbPath: string, filePath: string): ChunkInfo[] {
|
|
310
|
+
const db = openDb(dbPath);
|
|
311
|
+
try {
|
|
312
|
+
if (!tableExists(db, 'code_chunks')) return [];
|
|
313
|
+
|
|
314
|
+
const rows = db.prepare(`
|
|
315
|
+
SELECT id, chunk_type, name, start_line, end_line, content, language
|
|
316
|
+
FROM code_chunks
|
|
317
|
+
WHERE file_path = ?
|
|
318
|
+
ORDER BY start_line
|
|
319
|
+
`).all(filePath) as { id: number; chunk_type: string; name: string | null; start_line: number; end_line: number; content: string; language: string }[];
|
|
320
|
+
|
|
321
|
+
const hasCallEdges = tableExists(db, 'code_call_edges');
|
|
322
|
+
|
|
323
|
+
return rows.map(r => {
|
|
324
|
+
let callsOut: string[] = [];
|
|
325
|
+
let calledBy: string[] = [];
|
|
326
|
+
|
|
327
|
+
if (hasCallEdges) {
|
|
328
|
+
callsOut = (db.prepare(`
|
|
329
|
+
SELECT DISTINCT symbol_name FROM code_call_edges WHERE caller_chunk_id = ?
|
|
330
|
+
`).all(r.id) as { symbol_name: string }[]).map(row => row.symbol_name);
|
|
331
|
+
|
|
332
|
+
calledBy = (db.prepare(`
|
|
333
|
+
SELECT DISTINCT symbol_name FROM code_call_edges WHERE callee_chunk_id = ?
|
|
334
|
+
`).all(r.id) as { symbol_name: string }[]).map(row => row.symbol_name);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
return {
|
|
338
|
+
id: r.id,
|
|
339
|
+
chunkType: r.chunk_type,
|
|
340
|
+
name: r.name,
|
|
341
|
+
startLine: r.start_line,
|
|
342
|
+
endLine: r.end_line,
|
|
343
|
+
content: r.content,
|
|
344
|
+
language: r.language,
|
|
345
|
+
callsOut,
|
|
346
|
+
calledBy,
|
|
347
|
+
};
|
|
348
|
+
});
|
|
349
|
+
} finally {
|
|
350
|
+
db.close();
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
export function fetchCallTree(dbPath: string, chunkId: number, depth: number = 3): CallTreeNode {
|
|
355
|
+
const db = openDb(dbPath);
|
|
356
|
+
try {
|
|
357
|
+
const chunk = db.prepare(`
|
|
358
|
+
SELECT id, name, file_path FROM code_chunks WHERE id = ?
|
|
359
|
+
`).get(chunkId) as { id: number; name: string | null; file_path: string } | undefined;
|
|
360
|
+
|
|
361
|
+
if (!chunk) return { chunkId, symbol: '?', filePath: '?', children: [] };
|
|
362
|
+
|
|
363
|
+
function expand(id: number, d: number, visited: Set<number>): CallTreeNode[] {
|
|
364
|
+
if (d <= 0 || !tableExists(db, 'code_call_edges')) return [];
|
|
365
|
+
|
|
366
|
+
const edges = db.prepare(`
|
|
367
|
+
SELECT DISTINCT ce.callee_chunk_id, ce.symbol_name, cc.file_path
|
|
368
|
+
FROM code_call_edges ce
|
|
369
|
+
JOIN code_chunks cc ON cc.id = ce.callee_chunk_id
|
|
370
|
+
WHERE ce.caller_chunk_id = ?
|
|
371
|
+
ORDER BY ce.symbol_name
|
|
372
|
+
`).all(id) as { callee_chunk_id: number; symbol_name: string; file_path: string }[];
|
|
373
|
+
|
|
374
|
+
return edges
|
|
375
|
+
.filter(e => !visited.has(e.callee_chunk_id))
|
|
376
|
+
.map(e => {
|
|
377
|
+
visited.add(e.callee_chunk_id);
|
|
378
|
+
return {
|
|
379
|
+
chunkId: e.callee_chunk_id,
|
|
380
|
+
symbol: e.symbol_name,
|
|
381
|
+
filePath: e.file_path,
|
|
382
|
+
children: expand(e.callee_chunk_id, d - 1, visited),
|
|
383
|
+
};
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const visited = new Set([chunkId]);
|
|
388
|
+
return {
|
|
389
|
+
chunkId,
|
|
390
|
+
symbol: chunk.name ?? 'anonymous',
|
|
391
|
+
filePath: chunk.file_path,
|
|
392
|
+
children: expand(chunkId, depth, visited),
|
|
393
|
+
};
|
|
394
|
+
} finally {
|
|
395
|
+
db.close();
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/** Search for chunks by symbol name — used by call graph view. */
|
|
400
|
+
export function searchSymbols(dbPath: string, query: string, limit: number = 10): { id: number; name: string; filePath: string }[] {
|
|
401
|
+
const db = openDb(dbPath);
|
|
402
|
+
try {
|
|
403
|
+
if (!tableExists(db, 'code_chunks')) return [];
|
|
404
|
+
return (db.prepare(`
|
|
405
|
+
SELECT id, name, file_path as filePath FROM code_chunks
|
|
406
|
+
WHERE name LIKE ? AND name IS NOT NULL AND name != ''
|
|
407
|
+
ORDER BY name LIMIT ?
|
|
408
|
+
`).all(`%${query}%`, limit) as { id: number; name: string; filePath: string }[]);
|
|
409
|
+
} finally {
|
|
410
|
+
db.close();
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/** Search result for full-text search. */
|
|
415
|
+
export interface SearchResultItem {
|
|
416
|
+
id: number;
|
|
417
|
+
filePath: string;
|
|
418
|
+
name: string | null;
|
|
419
|
+
chunkType: string;
|
|
420
|
+
startLine: number;
|
|
421
|
+
endLine: number;
|
|
422
|
+
language: string;
|
|
423
|
+
matchContext: string;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/** Full-text search across chunk content, names, file paths. Returns matching chunks with context. */
|
|
427
|
+
export function searchChunks(dbPath: string, query: string, limit: number = 30): SearchResultItem[] {
|
|
428
|
+
const db = openDb(dbPath);
|
|
429
|
+
try {
|
|
430
|
+
if (!tableExists(db, 'code_chunks')) return [];
|
|
431
|
+
|
|
432
|
+
// Try FTS first (if fts_code exists)
|
|
433
|
+
if (tableExists(db, 'fts_code')) {
|
|
434
|
+
try {
|
|
435
|
+
const rows = db.prepare(`
|
|
436
|
+
SELECT cc.id, cc.file_path, cc.name, cc.chunk_type, cc.start_line, cc.end_line, cc.language,
|
|
437
|
+
snippet(fts_code, 0, '>>>', '<<<', '...', 40) as match_ctx
|
|
438
|
+
FROM fts_code ft
|
|
439
|
+
JOIN code_chunks cc ON cc.id = ft.rowid
|
|
440
|
+
WHERE fts_code MATCH ?
|
|
441
|
+
ORDER BY rank
|
|
442
|
+
LIMIT ?
|
|
443
|
+
`).all(query, limit) as unknown as { id: number; file_path: string; name: string | null; chunk_type: string; start_line: number; end_line: number; language: string; match_ctx: string }[];
|
|
444
|
+
|
|
445
|
+
return rows.map(r => ({
|
|
446
|
+
id: r.id,
|
|
447
|
+
filePath: r.file_path,
|
|
448
|
+
name: r.name,
|
|
449
|
+
chunkType: r.chunk_type,
|
|
450
|
+
startLine: r.start_line,
|
|
451
|
+
endLine: r.end_line,
|
|
452
|
+
language: r.language,
|
|
453
|
+
matchContext: r.match_ctx,
|
|
454
|
+
}));
|
|
455
|
+
} catch {
|
|
456
|
+
// FTS query parse error — fall through to LIKE
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// Fallback: LIKE search across name, file_path, content
|
|
461
|
+
const rows = db.prepare(`
|
|
462
|
+
SELECT id, file_path, name, chunk_type, start_line, end_line, language,
|
|
463
|
+
SUBSTR(content, MAX(1, INSTR(LOWER(content), LOWER(?)) - 40), 100) as match_ctx
|
|
464
|
+
FROM code_chunks
|
|
465
|
+
WHERE chunk_type != 'synopsis'
|
|
466
|
+
AND (LOWER(name) LIKE LOWER(?) OR LOWER(file_path) LIKE LOWER(?) OR LOWER(content) LIKE LOWER(?))
|
|
467
|
+
ORDER BY
|
|
468
|
+
CASE WHEN LOWER(name) LIKE LOWER(?) THEN 0
|
|
469
|
+
WHEN LOWER(file_path) LIKE LOWER(?) THEN 1
|
|
470
|
+
ELSE 2 END,
|
|
471
|
+
file_path, start_line
|
|
472
|
+
LIMIT ?
|
|
473
|
+
`).all(query, `%${query}%`, `%${query}%`, `%${query}%`, `%${query}%`, `%${query}%`, limit) as unknown as { id: number; file_path: string; name: string | null; chunk_type: string; start_line: number; end_line: number; language: string; match_ctx: string }[];
|
|
474
|
+
|
|
475
|
+
return rows.map(r => ({
|
|
476
|
+
id: r.id,
|
|
477
|
+
filePath: r.file_path,
|
|
478
|
+
name: r.name,
|
|
479
|
+
chunkType: r.chunk_type,
|
|
480
|
+
startLine: r.start_line,
|
|
481
|
+
endLine: r.end_line,
|
|
482
|
+
language: r.language,
|
|
483
|
+
matchContext: r.match_ctx || '',
|
|
484
|
+
}));
|
|
485
|
+
} finally {
|
|
486
|
+
db.close();
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/** Fetch a single chunk by ID. */
|
|
491
|
+
export function fetchChunkById(dbPath: string, chunkId: number): ChunkInfo | null {
|
|
492
|
+
const db = openDb(dbPath);
|
|
493
|
+
try {
|
|
494
|
+
const r = db.prepare(`
|
|
495
|
+
SELECT id, chunk_type, name, start_line, end_line, content, language
|
|
496
|
+
FROM code_chunks WHERE id = ?
|
|
497
|
+
`).get(chunkId) as { id: number; chunk_type: string; name: string | null; start_line: number; end_line: number; content: string; language: string } | undefined;
|
|
498
|
+
|
|
499
|
+
if (!r) return null;
|
|
500
|
+
|
|
501
|
+
let callsOut: string[] = [];
|
|
502
|
+
let calledBy: string[] = [];
|
|
503
|
+
if (tableExists(db, 'code_call_edges')) {
|
|
504
|
+
callsOut = (db.prepare(`SELECT DISTINCT symbol_name FROM code_call_edges WHERE caller_chunk_id = ?`).all(r.id) as { symbol_name: string }[]).map(row => row.symbol_name);
|
|
505
|
+
calledBy = (db.prepare(`SELECT DISTINCT symbol_name FROM code_call_edges WHERE callee_chunk_id = ?`).all(r.id) as { symbol_name: string }[]).map(row => row.symbol_name);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
return {
|
|
509
|
+
id: r.id,
|
|
510
|
+
chunkType: r.chunk_type,
|
|
511
|
+
name: r.name,
|
|
512
|
+
startLine: r.start_line,
|
|
513
|
+
endLine: r.end_line,
|
|
514
|
+
content: r.content,
|
|
515
|
+
language: r.language,
|
|
516
|
+
callsOut,
|
|
517
|
+
calledBy,
|
|
518
|
+
};
|
|
519
|
+
} finally {
|
|
520
|
+
db.close();
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|