@veewo/gitnexus 1.3.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 +234 -0
- package/dist/benchmark/agent-context/evaluators.d.ts +9 -0
- package/dist/benchmark/agent-context/evaluators.js +196 -0
- package/dist/benchmark/agent-context/evaluators.test.d.ts +1 -0
- package/dist/benchmark/agent-context/evaluators.test.js +39 -0
- package/dist/benchmark/agent-context/io.d.ts +2 -0
- package/dist/benchmark/agent-context/io.js +23 -0
- package/dist/benchmark/agent-context/io.test.d.ts +1 -0
- package/dist/benchmark/agent-context/io.test.js +19 -0
- package/dist/benchmark/agent-context/report.d.ts +2 -0
- package/dist/benchmark/agent-context/report.js +59 -0
- package/dist/benchmark/agent-context/report.test.d.ts +1 -0
- package/dist/benchmark/agent-context/report.test.js +85 -0
- package/dist/benchmark/agent-context/runner.d.ts +46 -0
- package/dist/benchmark/agent-context/runner.js +111 -0
- package/dist/benchmark/agent-context/runner.test.d.ts +1 -0
- package/dist/benchmark/agent-context/runner.test.js +79 -0
- package/dist/benchmark/agent-context/tool-runner.d.ts +7 -0
- package/dist/benchmark/agent-context/tool-runner.js +18 -0
- package/dist/benchmark/agent-context/tool-runner.test.d.ts +1 -0
- package/dist/benchmark/agent-context/tool-runner.test.js +11 -0
- package/dist/benchmark/agent-context/types.d.ts +40 -0
- package/dist/benchmark/agent-context/types.js +1 -0
- package/dist/benchmark/analyze-runner.d.ts +16 -0
- package/dist/benchmark/analyze-runner.js +51 -0
- package/dist/benchmark/analyze-runner.test.d.ts +1 -0
- package/dist/benchmark/analyze-runner.test.js +37 -0
- package/dist/benchmark/evaluators.d.ts +6 -0
- package/dist/benchmark/evaluators.js +10 -0
- package/dist/benchmark/evaluators.test.d.ts +1 -0
- package/dist/benchmark/evaluators.test.js +12 -0
- package/dist/benchmark/io.d.ts +7 -0
- package/dist/benchmark/io.js +25 -0
- package/dist/benchmark/io.test.d.ts +1 -0
- package/dist/benchmark/io.test.js +35 -0
- package/dist/benchmark/neonspark-candidates.d.ts +19 -0
- package/dist/benchmark/neonspark-candidates.js +94 -0
- package/dist/benchmark/neonspark-candidates.test.d.ts +1 -0
- package/dist/benchmark/neonspark-candidates.test.js +43 -0
- package/dist/benchmark/neonspark-materialize.d.ts +19 -0
- package/dist/benchmark/neonspark-materialize.js +111 -0
- package/dist/benchmark/neonspark-materialize.test.d.ts +1 -0
- package/dist/benchmark/neonspark-materialize.test.js +124 -0
- package/dist/benchmark/neonspark-sync.d.ts +3 -0
- package/dist/benchmark/neonspark-sync.js +53 -0
- package/dist/benchmark/neonspark-sync.test.d.ts +1 -0
- package/dist/benchmark/neonspark-sync.test.js +20 -0
- package/dist/benchmark/report.d.ts +1 -0
- package/dist/benchmark/report.js +7 -0
- package/dist/benchmark/runner.d.ts +48 -0
- package/dist/benchmark/runner.js +302 -0
- package/dist/benchmark/runner.test.d.ts +1 -0
- package/dist/benchmark/runner.test.js +50 -0
- package/dist/benchmark/scoring.d.ts +16 -0
- package/dist/benchmark/scoring.js +27 -0
- package/dist/benchmark/scoring.test.d.ts +1 -0
- package/dist/benchmark/scoring.test.js +24 -0
- package/dist/benchmark/tool-runner.d.ts +6 -0
- package/dist/benchmark/tool-runner.js +17 -0
- package/dist/benchmark/types.d.ts +36 -0
- package/dist/benchmark/types.js +1 -0
- package/dist/cli/ai-context.d.ts +22 -0
- package/dist/cli/ai-context.js +184 -0
- package/dist/cli/ai-context.test.d.ts +1 -0
- package/dist/cli/ai-context.test.js +30 -0
- package/dist/cli/analyze-multi-scope-regression.test.d.ts +1 -0
- package/dist/cli/analyze-multi-scope-regression.test.js +22 -0
- package/dist/cli/analyze-options.d.ts +7 -0
- package/dist/cli/analyze-options.js +56 -0
- package/dist/cli/analyze-options.test.d.ts +1 -0
- package/dist/cli/analyze-options.test.js +36 -0
- package/dist/cli/analyze.d.ts +14 -0
- package/dist/cli/analyze.js +384 -0
- package/dist/cli/augment.d.ts +13 -0
- package/dist/cli/augment.js +33 -0
- package/dist/cli/benchmark-agent-context.d.ts +29 -0
- package/dist/cli/benchmark-agent-context.js +61 -0
- package/dist/cli/benchmark-agent-context.test.d.ts +1 -0
- package/dist/cli/benchmark-agent-context.test.js +80 -0
- package/dist/cli/benchmark-unity.d.ts +15 -0
- package/dist/cli/benchmark-unity.js +31 -0
- package/dist/cli/benchmark-unity.test.d.ts +1 -0
- package/dist/cli/benchmark-unity.test.js +18 -0
- package/dist/cli/claude-hooks.d.ts +22 -0
- package/dist/cli/claude-hooks.js +97 -0
- package/dist/cli/clean.d.ts +10 -0
- package/dist/cli/clean.js +60 -0
- package/dist/cli/eval-server.d.ts +30 -0
- package/dist/cli/eval-server.js +372 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +182 -0
- package/dist/cli/list.d.ts +6 -0
- package/dist/cli/list.js +33 -0
- package/dist/cli/mcp.d.ts +8 -0
- package/dist/cli/mcp.js +34 -0
- package/dist/cli/repo-manager-alias.test.d.ts +1 -0
- package/dist/cli/repo-manager-alias.test.js +40 -0
- package/dist/cli/scope-filter.test.d.ts +1 -0
- package/dist/cli/scope-filter.test.js +49 -0
- package/dist/cli/serve.d.ts +4 -0
- package/dist/cli/serve.js +6 -0
- package/dist/cli/setup.d.ts +8 -0
- package/dist/cli/setup.js +311 -0
- package/dist/cli/setup.test.d.ts +1 -0
- package/dist/cli/setup.test.js +31 -0
- package/dist/cli/status.d.ts +6 -0
- package/dist/cli/status.js +27 -0
- package/dist/cli/tool.d.ts +40 -0
- package/dist/cli/tool.js +94 -0
- package/dist/cli/version.test.d.ts +1 -0
- package/dist/cli/version.test.js +19 -0
- package/dist/cli/wiki.d.ts +15 -0
- package/dist/cli/wiki.js +361 -0
- package/dist/config/ignore-service.d.ts +1 -0
- package/dist/config/ignore-service.js +210 -0
- package/dist/config/supported-languages.d.ts +12 -0
- package/dist/config/supported-languages.js +15 -0
- package/dist/core/augmentation/engine.d.ts +26 -0
- package/dist/core/augmentation/engine.js +213 -0
- package/dist/core/embeddings/embedder.d.ts +60 -0
- package/dist/core/embeddings/embedder.js +251 -0
- package/dist/core/embeddings/embedding-pipeline.d.ts +51 -0
- package/dist/core/embeddings/embedding-pipeline.js +329 -0
- package/dist/core/embeddings/index.d.ts +9 -0
- package/dist/core/embeddings/index.js +9 -0
- package/dist/core/embeddings/text-generator.d.ts +24 -0
- package/dist/core/embeddings/text-generator.js +182 -0
- package/dist/core/embeddings/types.d.ts +87 -0
- package/dist/core/embeddings/types.js +32 -0
- package/dist/core/graph/graph.d.ts +2 -0
- package/dist/core/graph/graph.js +66 -0
- package/dist/core/graph/types.d.ts +61 -0
- package/dist/core/graph/types.js +1 -0
- package/dist/core/ingestion/ast-cache.d.ts +11 -0
- package/dist/core/ingestion/ast-cache.js +34 -0
- package/dist/core/ingestion/call-processor.d.ts +15 -0
- package/dist/core/ingestion/call-processor.js +327 -0
- package/dist/core/ingestion/cluster-enricher.d.ts +38 -0
- package/dist/core/ingestion/cluster-enricher.js +170 -0
- package/dist/core/ingestion/community-processor.d.ts +39 -0
- package/dist/core/ingestion/community-processor.js +312 -0
- package/dist/core/ingestion/entry-point-scoring.d.ts +39 -0
- package/dist/core/ingestion/entry-point-scoring.js +260 -0
- package/dist/core/ingestion/filesystem-walker.d.ts +28 -0
- package/dist/core/ingestion/filesystem-walker.js +80 -0
- package/dist/core/ingestion/framework-detection.d.ts +39 -0
- package/dist/core/ingestion/framework-detection.js +235 -0
- package/dist/core/ingestion/heritage-processor.d.ts +20 -0
- package/dist/core/ingestion/heritage-processor.js +197 -0
- package/dist/core/ingestion/import-processor.d.ts +38 -0
- package/dist/core/ingestion/import-processor.js +778 -0
- package/dist/core/ingestion/parsing-processor.d.ts +15 -0
- package/dist/core/ingestion/parsing-processor.js +291 -0
- package/dist/core/ingestion/pipeline.d.ts +5 -0
- package/dist/core/ingestion/pipeline.js +323 -0
- package/dist/core/ingestion/process-processor.d.ts +51 -0
- package/dist/core/ingestion/process-processor.js +309 -0
- package/dist/core/ingestion/scope-filter.d.ts +25 -0
- package/dist/core/ingestion/scope-filter.js +100 -0
- package/dist/core/ingestion/structure-processor.d.ts +2 -0
- package/dist/core/ingestion/structure-processor.js +36 -0
- package/dist/core/ingestion/symbol-table.d.ts +33 -0
- package/dist/core/ingestion/symbol-table.js +38 -0
- package/dist/core/ingestion/tree-sitter-queries.d.ts +12 -0
- package/dist/core/ingestion/tree-sitter-queries.js +398 -0
- package/dist/core/ingestion/utils.d.ts +10 -0
- package/dist/core/ingestion/utils.js +50 -0
- package/dist/core/ingestion/workers/parse-worker.d.ts +59 -0
- package/dist/core/ingestion/workers/parse-worker.js +672 -0
- package/dist/core/ingestion/workers/worker-pool.d.ts +16 -0
- package/dist/core/ingestion/workers/worker-pool.js +120 -0
- package/dist/core/kuzu/csv-generator.d.ts +29 -0
- package/dist/core/kuzu/csv-generator.js +336 -0
- package/dist/core/kuzu/kuzu-adapter.d.ts +101 -0
- package/dist/core/kuzu/kuzu-adapter.js +753 -0
- package/dist/core/kuzu/schema.d.ts +53 -0
- package/dist/core/kuzu/schema.js +407 -0
- package/dist/core/search/bm25-index.d.ts +23 -0
- package/dist/core/search/bm25-index.js +95 -0
- package/dist/core/search/hybrid-search.d.ts +49 -0
- package/dist/core/search/hybrid-search.js +118 -0
- package/dist/core/tree-sitter/parser-loader.d.ts +4 -0
- package/dist/core/tree-sitter/parser-loader.js +44 -0
- package/dist/core/wiki/generator.d.ts +110 -0
- package/dist/core/wiki/generator.js +786 -0
- package/dist/core/wiki/graph-queries.d.ts +80 -0
- package/dist/core/wiki/graph-queries.js +238 -0
- package/dist/core/wiki/html-viewer.d.ts +10 -0
- package/dist/core/wiki/html-viewer.js +297 -0
- package/dist/core/wiki/llm-client.d.ts +40 -0
- package/dist/core/wiki/llm-client.js +162 -0
- package/dist/core/wiki/prompts.d.ts +53 -0
- package/dist/core/wiki/prompts.js +174 -0
- package/dist/lib/utils.d.ts +1 -0
- package/dist/lib/utils.js +3 -0
- package/dist/mcp/core/embedder.d.ts +27 -0
- package/dist/mcp/core/embedder.js +108 -0
- package/dist/mcp/core/kuzu-adapter.d.ts +34 -0
- package/dist/mcp/core/kuzu-adapter.js +231 -0
- package/dist/mcp/local/local-backend.d.ts +160 -0
- package/dist/mcp/local/local-backend.js +1646 -0
- package/dist/mcp/resources.d.ts +31 -0
- package/dist/mcp/resources.js +407 -0
- package/dist/mcp/server.d.ts +23 -0
- package/dist/mcp/server.js +251 -0
- package/dist/mcp/staleness.d.ts +15 -0
- package/dist/mcp/staleness.js +29 -0
- package/dist/mcp/tools.d.ts +24 -0
- package/dist/mcp/tools.js +195 -0
- package/dist/server/api.d.ts +10 -0
- package/dist/server/api.js +344 -0
- package/dist/server/mcp-http.d.ts +13 -0
- package/dist/server/mcp-http.js +100 -0
- package/dist/storage/git.d.ts +6 -0
- package/dist/storage/git.js +32 -0
- package/dist/storage/repo-manager.d.ts +125 -0
- package/dist/storage/repo-manager.js +257 -0
- package/dist/types/pipeline.d.ts +34 -0
- package/dist/types/pipeline.js +18 -0
- package/hooks/claude/gitnexus-hook.cjs +135 -0
- package/hooks/claude/pre-tool-use.sh +78 -0
- package/hooks/claude/session-start.sh +42 -0
- package/package.json +92 -0
- package/skills/gitnexus-cli.md +82 -0
- package/skills/gitnexus-debugging.md +89 -0
- package/skills/gitnexus-exploring.md +78 -0
- package/skills/gitnexus-guide.md +64 -0
- package/skills/gitnexus-impact-analysis.md +97 -0
- package/skills/gitnexus-refactoring.md +121 -0
- package/vendor/leiden/index.cjs +355 -0
- package/vendor/leiden/utils.cjs +392 -0
|
@@ -0,0 +1,753 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import { createReadStream } from 'fs';
|
|
3
|
+
import { createInterface } from 'readline';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import kuzu from 'kuzu';
|
|
6
|
+
import { NODE_TABLES, REL_TABLE_NAME, SCHEMA_QUERIES, EMBEDDING_TABLE_NAME, } from './schema.js';
|
|
7
|
+
import { streamAllCSVsToDisk } from './csv-generator.js';
|
|
8
|
+
let db = null;
|
|
9
|
+
let conn = null;
|
|
10
|
+
let currentDbPath = null;
|
|
11
|
+
let ftsLoaded = false;
|
|
12
|
+
// Global session lock for operations that touch module-level kuzu globals.
|
|
13
|
+
// This guarantees no DB switch can happen while an operation is running.
|
|
14
|
+
let sessionLock = Promise.resolve();
|
|
15
|
+
const runWithSessionLock = async (operation) => {
|
|
16
|
+
const previous = sessionLock;
|
|
17
|
+
let release = null;
|
|
18
|
+
sessionLock = new Promise(resolve => {
|
|
19
|
+
release = resolve;
|
|
20
|
+
});
|
|
21
|
+
await previous;
|
|
22
|
+
try {
|
|
23
|
+
return await operation();
|
|
24
|
+
}
|
|
25
|
+
finally {
|
|
26
|
+
release?.();
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
const normalizeCopyPath = (filePath) => filePath.replace(/\\/g, '/');
|
|
30
|
+
export const initKuzu = async (dbPath) => {
|
|
31
|
+
return runWithSessionLock(() => ensureKuzuInitialized(dbPath));
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Execute multiple queries against one repo DB atomically.
|
|
35
|
+
* While the callback runs, no other request can switch the active DB.
|
|
36
|
+
*/
|
|
37
|
+
export const withKuzuDb = async (dbPath, operation) => {
|
|
38
|
+
return runWithSessionLock(async () => {
|
|
39
|
+
await ensureKuzuInitialized(dbPath);
|
|
40
|
+
return operation();
|
|
41
|
+
});
|
|
42
|
+
};
|
|
43
|
+
const ensureKuzuInitialized = async (dbPath) => {
|
|
44
|
+
if (conn && currentDbPath === dbPath) {
|
|
45
|
+
return { db, conn };
|
|
46
|
+
}
|
|
47
|
+
await doInitKuzu(dbPath);
|
|
48
|
+
return { db, conn };
|
|
49
|
+
};
|
|
50
|
+
const doInitKuzu = async (dbPath) => {
|
|
51
|
+
// Different database requested — close the old one first
|
|
52
|
+
if (conn || db) {
|
|
53
|
+
try {
|
|
54
|
+
if (conn)
|
|
55
|
+
await conn.close();
|
|
56
|
+
}
|
|
57
|
+
catch { }
|
|
58
|
+
try {
|
|
59
|
+
if (db)
|
|
60
|
+
await db.close();
|
|
61
|
+
}
|
|
62
|
+
catch { }
|
|
63
|
+
conn = null;
|
|
64
|
+
db = null;
|
|
65
|
+
currentDbPath = null;
|
|
66
|
+
ftsLoaded = false;
|
|
67
|
+
}
|
|
68
|
+
// kuzu v0.11 stores the database as a single file (not a directory).
|
|
69
|
+
// If the path already exists, it must be a valid kuzu database file.
|
|
70
|
+
// Remove stale empty directories or files from older versions.
|
|
71
|
+
try {
|
|
72
|
+
const stat = await fs.stat(dbPath);
|
|
73
|
+
if (stat.isDirectory()) {
|
|
74
|
+
// Old-style directory database or empty leftover - remove it
|
|
75
|
+
const files = await fs.readdir(dbPath);
|
|
76
|
+
if (files.length === 0) {
|
|
77
|
+
await fs.rmdir(dbPath);
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
// Non-empty directory from older kuzu version - remove entire directory
|
|
81
|
+
await fs.rm(dbPath, { recursive: true, force: true });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
// If it's a file, assume it's an existing kuzu database - kuzu will open it
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
// Path doesn't exist, which is what kuzu wants for a new database
|
|
88
|
+
}
|
|
89
|
+
// Ensure parent directory exists
|
|
90
|
+
const parentDir = path.dirname(dbPath);
|
|
91
|
+
await fs.mkdir(parentDir, { recursive: true });
|
|
92
|
+
db = new kuzu.Database(dbPath);
|
|
93
|
+
conn = new kuzu.Connection(db);
|
|
94
|
+
for (const schemaQuery of SCHEMA_QUERIES) {
|
|
95
|
+
try {
|
|
96
|
+
await conn.query(schemaQuery);
|
|
97
|
+
}
|
|
98
|
+
catch (err) {
|
|
99
|
+
// Only ignore "already exists" errors - log everything else
|
|
100
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
101
|
+
if (!msg.includes('already exists')) {
|
|
102
|
+
console.warn(`⚠️ Schema creation warning: ${msg.slice(0, 120)}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
currentDbPath = dbPath;
|
|
107
|
+
return { db, conn };
|
|
108
|
+
};
|
|
109
|
+
export const loadGraphToKuzu = async (graph, repoPath, storagePath, onProgress) => {
|
|
110
|
+
if (!conn) {
|
|
111
|
+
throw new Error('KuzuDB not initialized. Call initKuzu first.');
|
|
112
|
+
}
|
|
113
|
+
const log = onProgress || (() => { });
|
|
114
|
+
const csvDir = path.join(storagePath, 'csv');
|
|
115
|
+
log('Streaming CSVs to disk...');
|
|
116
|
+
const csvResult = await streamAllCSVsToDisk(graph, repoPath, csvDir);
|
|
117
|
+
const validTables = new Set(NODE_TABLES);
|
|
118
|
+
const getNodeLabel = (nodeId) => {
|
|
119
|
+
if (nodeId.startsWith('comm_'))
|
|
120
|
+
return 'Community';
|
|
121
|
+
if (nodeId.startsWith('proc_'))
|
|
122
|
+
return 'Process';
|
|
123
|
+
return nodeId.split(':')[0];
|
|
124
|
+
};
|
|
125
|
+
// Bulk COPY all node CSVs (sequential — KuzuDB allows only one write txn at a time)
|
|
126
|
+
const nodeFiles = [...csvResult.nodeFiles.entries()];
|
|
127
|
+
const totalSteps = nodeFiles.length + 1; // +1 for relationships
|
|
128
|
+
let stepsDone = 0;
|
|
129
|
+
for (const [table, { csvPath, rows }] of nodeFiles) {
|
|
130
|
+
stepsDone++;
|
|
131
|
+
log(`Loading nodes ${stepsDone}/${totalSteps}: ${table} (${rows.toLocaleString()} rows)`);
|
|
132
|
+
const normalizedPath = normalizeCopyPath(csvPath);
|
|
133
|
+
const copyQuery = getCopyQuery(table, normalizedPath);
|
|
134
|
+
try {
|
|
135
|
+
await conn.query(copyQuery);
|
|
136
|
+
}
|
|
137
|
+
catch (err) {
|
|
138
|
+
try {
|
|
139
|
+
const retryQuery = copyQuery.replace('auto_detect=false)', 'auto_detect=false, IGNORE_ERRORS=true)');
|
|
140
|
+
await conn.query(retryQuery);
|
|
141
|
+
}
|
|
142
|
+
catch (retryErr) {
|
|
143
|
+
const retryMsg = retryErr instanceof Error ? retryErr.message : String(retryErr);
|
|
144
|
+
throw new Error(`COPY failed for ${table}: ${retryMsg.slice(0, 200)}`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
// Bulk COPY relationships — split by FROM→TO label pair (KuzuDB requires it)
|
|
149
|
+
// Stream-read the relation CSV line by line to avoid exceeding V8 max string length
|
|
150
|
+
let relHeader = '';
|
|
151
|
+
const relsByPair = new Map();
|
|
152
|
+
let skippedRels = 0;
|
|
153
|
+
let totalValidRels = 0;
|
|
154
|
+
await new Promise((resolve, reject) => {
|
|
155
|
+
const rl = createInterface({ input: createReadStream(csvResult.relCsvPath, 'utf-8'), crlfDelay: Infinity });
|
|
156
|
+
let isFirst = true;
|
|
157
|
+
rl.on('line', (line) => {
|
|
158
|
+
if (isFirst) {
|
|
159
|
+
relHeader = line;
|
|
160
|
+
isFirst = false;
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
if (!line.trim())
|
|
164
|
+
return;
|
|
165
|
+
const match = line.match(/"([^"]*)","([^"]*)"/);
|
|
166
|
+
if (!match) {
|
|
167
|
+
skippedRels++;
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
const fromLabel = getNodeLabel(match[1]);
|
|
171
|
+
const toLabel = getNodeLabel(match[2]);
|
|
172
|
+
if (!validTables.has(fromLabel) || !validTables.has(toLabel)) {
|
|
173
|
+
skippedRels++;
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
const pairKey = `${fromLabel}|${toLabel}`;
|
|
177
|
+
let list = relsByPair.get(pairKey);
|
|
178
|
+
if (!list) {
|
|
179
|
+
list = [];
|
|
180
|
+
relsByPair.set(pairKey, list);
|
|
181
|
+
}
|
|
182
|
+
list.push(line);
|
|
183
|
+
totalValidRels++;
|
|
184
|
+
});
|
|
185
|
+
rl.on('close', resolve);
|
|
186
|
+
rl.on('error', reject);
|
|
187
|
+
});
|
|
188
|
+
const insertedRels = totalValidRels;
|
|
189
|
+
const warnings = [];
|
|
190
|
+
if (insertedRels > 0) {
|
|
191
|
+
log(`Loading edges: ${insertedRels.toLocaleString()} across ${relsByPair.size} types`);
|
|
192
|
+
let pairIdx = 0;
|
|
193
|
+
let failedPairEdges = 0;
|
|
194
|
+
const failedPairLines = [];
|
|
195
|
+
for (const [pairKey, lines] of relsByPair) {
|
|
196
|
+
pairIdx++;
|
|
197
|
+
const [fromLabel, toLabel] = pairKey.split('|');
|
|
198
|
+
const pairCsvPath = path.join(csvDir, `rel_${fromLabel}_${toLabel}.csv`);
|
|
199
|
+
await fs.writeFile(pairCsvPath, relHeader + '\n' + lines.join('\n'), 'utf-8');
|
|
200
|
+
const normalizedPath = normalizeCopyPath(pairCsvPath);
|
|
201
|
+
const copyQuery = `COPY ${REL_TABLE_NAME} FROM "${normalizedPath}" (from="${fromLabel}", to="${toLabel}", HEADER=true, ESCAPE='"', DELIM=',', QUOTE='"', PARALLEL=false, auto_detect=false)`;
|
|
202
|
+
if (pairIdx % 5 === 0 || lines.length > 1000) {
|
|
203
|
+
log(`Loading edges: ${pairIdx}/${relsByPair.size} types (${fromLabel} -> ${toLabel})`);
|
|
204
|
+
}
|
|
205
|
+
try {
|
|
206
|
+
await conn.query(copyQuery);
|
|
207
|
+
}
|
|
208
|
+
catch (err) {
|
|
209
|
+
try {
|
|
210
|
+
const retryQuery = copyQuery.replace('auto_detect=false)', 'auto_detect=false, IGNORE_ERRORS=true)');
|
|
211
|
+
await conn.query(retryQuery);
|
|
212
|
+
}
|
|
213
|
+
catch (retryErr) {
|
|
214
|
+
const retryMsg = retryErr instanceof Error ? retryErr.message : String(retryErr);
|
|
215
|
+
warnings.push(`${fromLabel}->${toLabel} (${lines.length} edges): ${retryMsg.slice(0, 80)}`);
|
|
216
|
+
failedPairEdges += lines.length;
|
|
217
|
+
failedPairLines.push(...lines);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
try {
|
|
221
|
+
await fs.unlink(pairCsvPath);
|
|
222
|
+
}
|
|
223
|
+
catch { }
|
|
224
|
+
}
|
|
225
|
+
if (failedPairLines.length > 0) {
|
|
226
|
+
log(`Inserting ${failedPairEdges} edges individually (missing schema pairs)`);
|
|
227
|
+
await fallbackRelationshipInserts([relHeader, ...failedPairLines], validTables, getNodeLabel);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
// Cleanup all CSVs
|
|
231
|
+
try {
|
|
232
|
+
await fs.unlink(csvResult.relCsvPath);
|
|
233
|
+
}
|
|
234
|
+
catch { }
|
|
235
|
+
for (const [, { csvPath }] of csvResult.nodeFiles) {
|
|
236
|
+
try {
|
|
237
|
+
await fs.unlink(csvPath);
|
|
238
|
+
}
|
|
239
|
+
catch { }
|
|
240
|
+
}
|
|
241
|
+
try {
|
|
242
|
+
const remaining = await fs.readdir(csvDir);
|
|
243
|
+
for (const f of remaining) {
|
|
244
|
+
try {
|
|
245
|
+
await fs.unlink(path.join(csvDir, f));
|
|
246
|
+
}
|
|
247
|
+
catch { }
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
catch { }
|
|
251
|
+
try {
|
|
252
|
+
await fs.rmdir(csvDir);
|
|
253
|
+
}
|
|
254
|
+
catch { }
|
|
255
|
+
return { success: true, insertedRels, skippedRels, warnings };
|
|
256
|
+
};
|
|
257
|
+
// KuzuDB default ESCAPE is '\' (backslash), but our CSV uses RFC 4180 escaping ("" for literal quotes).
|
|
258
|
+
// Source code content is full of backslashes which confuse the auto-detection.
|
|
259
|
+
// We MUST explicitly set ESCAPE='"' to use RFC 4180 escaping, and disable auto_detect to prevent
|
|
260
|
+
// KuzuDB from overriding our settings based on sample rows.
|
|
261
|
+
const COPY_CSV_OPTS = `(HEADER=true, ESCAPE='"', DELIM=',', QUOTE='"', PARALLEL=false, auto_detect=false)`;
|
|
262
|
+
// Multi-language table names that were created with backticks in CODE_ELEMENT_BASE
|
|
263
|
+
// and must always be referenced with backticks in queries
|
|
264
|
+
const BACKTICK_TABLES = new Set([
|
|
265
|
+
'Struct', 'Enum', 'Macro', 'Typedef', 'Union', 'Namespace', 'Trait', 'Impl',
|
|
266
|
+
'TypeAlias', 'Const', 'Static', 'Property', 'Record', 'Delegate', 'Annotation',
|
|
267
|
+
'Constructor', 'Template', 'Module',
|
|
268
|
+
]);
|
|
269
|
+
const escapeTableName = (table) => {
|
|
270
|
+
return BACKTICK_TABLES.has(table) ? `\`${table}\`` : table;
|
|
271
|
+
};
|
|
272
|
+
/** Fallback: insert relationships one-by-one if COPY fails */
|
|
273
|
+
const fallbackRelationshipInserts = async (validRelLines, validTables, getNodeLabel) => {
|
|
274
|
+
if (!conn)
|
|
275
|
+
return;
|
|
276
|
+
const escapeLabel = (label) => {
|
|
277
|
+
return BACKTICK_TABLES.has(label) ? `\`${label}\`` : label;
|
|
278
|
+
};
|
|
279
|
+
for (let i = 1; i < validRelLines.length; i++) {
|
|
280
|
+
const line = validRelLines[i];
|
|
281
|
+
try {
|
|
282
|
+
const match = line.match(/"([^"]*)","([^"]*)","([^"]*)",([0-9.]+),"([^"]*)",([0-9-]+)/);
|
|
283
|
+
if (!match)
|
|
284
|
+
continue;
|
|
285
|
+
const [, fromId, toId, relType, confidenceStr, reason, stepStr] = match;
|
|
286
|
+
const fromLabel = getNodeLabel(fromId);
|
|
287
|
+
const toLabel = getNodeLabel(toId);
|
|
288
|
+
if (!validTables.has(fromLabel) || !validTables.has(toLabel))
|
|
289
|
+
continue;
|
|
290
|
+
const confidence = parseFloat(confidenceStr) || 1.0;
|
|
291
|
+
const step = parseInt(stepStr) || 0;
|
|
292
|
+
await conn.query(`
|
|
293
|
+
MATCH (a:${escapeLabel(fromLabel)} {id: '${fromId.replace(/'/g, "''")}' }),
|
|
294
|
+
(b:${escapeLabel(toLabel)} {id: '${toId.replace(/'/g, "''")}' })
|
|
295
|
+
CREATE (a)-[:${REL_TABLE_NAME} {type: '${relType}', confidence: ${confidence}, reason: '${reason.replace(/'/g, "''")}', step: ${step}}]->(b)
|
|
296
|
+
`);
|
|
297
|
+
}
|
|
298
|
+
catch {
|
|
299
|
+
// skip
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
};
|
|
303
|
+
/** Tables with isExported column (TypeScript/JS-native types) */
|
|
304
|
+
const TABLES_WITH_EXPORTED = new Set(['Function', 'Class', 'Interface', 'Method', 'CodeElement']);
|
|
305
|
+
const getCopyQuery = (table, filePath) => {
|
|
306
|
+
const t = escapeTableName(table);
|
|
307
|
+
if (table === 'File') {
|
|
308
|
+
return `COPY ${t}(id, name, filePath, content) FROM "${filePath}" ${COPY_CSV_OPTS}`;
|
|
309
|
+
}
|
|
310
|
+
if (table === 'Folder') {
|
|
311
|
+
return `COPY ${t}(id, name, filePath) FROM "${filePath}" ${COPY_CSV_OPTS}`;
|
|
312
|
+
}
|
|
313
|
+
if (table === 'Community') {
|
|
314
|
+
return `COPY ${t}(id, label, heuristicLabel, keywords, description, enrichedBy, cohesion, symbolCount) FROM "${filePath}" ${COPY_CSV_OPTS}`;
|
|
315
|
+
}
|
|
316
|
+
if (table === 'Process') {
|
|
317
|
+
return `COPY ${t}(id, label, heuristicLabel, processType, stepCount, communities, entryPointId, terminalId) FROM "${filePath}" ${COPY_CSV_OPTS}`;
|
|
318
|
+
}
|
|
319
|
+
// TypeScript/JS code element tables have isExported; multi-language tables do not
|
|
320
|
+
if (TABLES_WITH_EXPORTED.has(table)) {
|
|
321
|
+
return `COPY ${t}(id, name, filePath, startLine, endLine, isExported, content, description) FROM "${filePath}" ${COPY_CSV_OPTS}`;
|
|
322
|
+
}
|
|
323
|
+
// Multi-language tables (Struct, Impl, Trait, Macro, etc.)
|
|
324
|
+
return `COPY ${t}(id, name, filePath, startLine, endLine, content, description) FROM "${filePath}" ${COPY_CSV_OPTS}`;
|
|
325
|
+
};
|
|
326
|
+
/**
|
|
327
|
+
* Insert a single node to KuzuDB
|
|
328
|
+
* @param label - Node type (File, Function, Class, etc.)
|
|
329
|
+
* @param properties - Node properties
|
|
330
|
+
* @param dbPath - Path to KuzuDB database (optional if already initialized)
|
|
331
|
+
*/
|
|
332
|
+
export const insertNodeToKuzu = async (label, properties, dbPath) => {
|
|
333
|
+
// Use provided dbPath or fall back to module-level db
|
|
334
|
+
const targetDbPath = dbPath || (db ? undefined : null);
|
|
335
|
+
if (!targetDbPath && !db) {
|
|
336
|
+
throw new Error('KuzuDB not initialized. Provide dbPath or call initKuzu first.');
|
|
337
|
+
}
|
|
338
|
+
try {
|
|
339
|
+
const escapeValue = (v) => {
|
|
340
|
+
if (v === null || v === undefined)
|
|
341
|
+
return 'NULL';
|
|
342
|
+
if (typeof v === 'number')
|
|
343
|
+
return String(v);
|
|
344
|
+
// Escape backslashes first (for Windows paths), then single quotes
|
|
345
|
+
return `'${String(v).replace(/\\/g, '\\\\').replace(/'/g, "''")}'`;
|
|
346
|
+
};
|
|
347
|
+
// Build INSERT query based on node type
|
|
348
|
+
const t = escapeTableName(label);
|
|
349
|
+
let query;
|
|
350
|
+
if (label === 'File') {
|
|
351
|
+
query = `CREATE (n:File {id: ${escapeValue(properties.id)}, name: ${escapeValue(properties.name)}, filePath: ${escapeValue(properties.filePath)}, content: ${escapeValue(properties.content || '')}})`;
|
|
352
|
+
}
|
|
353
|
+
else if (label === 'Folder') {
|
|
354
|
+
query = `CREATE (n:Folder {id: ${escapeValue(properties.id)}, name: ${escapeValue(properties.name)}, filePath: ${escapeValue(properties.filePath)}})`;
|
|
355
|
+
}
|
|
356
|
+
else if (TABLES_WITH_EXPORTED.has(label)) {
|
|
357
|
+
const descPart = properties.description ? `, description: ${escapeValue(properties.description)}` : '';
|
|
358
|
+
query = `CREATE (n:${t} {id: ${escapeValue(properties.id)}, name: ${escapeValue(properties.name)}, filePath: ${escapeValue(properties.filePath)}, startLine: ${properties.startLine || 0}, endLine: ${properties.endLine || 0}, isExported: ${!!properties.isExported}, content: ${escapeValue(properties.content || '')}${descPart}})`;
|
|
359
|
+
}
|
|
360
|
+
else {
|
|
361
|
+
// Multi-language tables (Struct, Impl, Trait, Macro, etc.) — no isExported
|
|
362
|
+
const descPart = properties.description ? `, description: ${escapeValue(properties.description)}` : '';
|
|
363
|
+
query = `CREATE (n:${t} {id: ${escapeValue(properties.id)}, name: ${escapeValue(properties.name)}, filePath: ${escapeValue(properties.filePath)}, startLine: ${properties.startLine || 0}, endLine: ${properties.endLine || 0}, content: ${escapeValue(properties.content || '')}${descPart}})`;
|
|
364
|
+
}
|
|
365
|
+
// Use per-query connection if dbPath provided (avoids lock conflicts)
|
|
366
|
+
if (targetDbPath) {
|
|
367
|
+
const tempDb = new kuzu.Database(targetDbPath);
|
|
368
|
+
const tempConn = new kuzu.Connection(tempDb);
|
|
369
|
+
try {
|
|
370
|
+
await tempConn.query(query);
|
|
371
|
+
return true;
|
|
372
|
+
}
|
|
373
|
+
finally {
|
|
374
|
+
try {
|
|
375
|
+
await tempConn.close();
|
|
376
|
+
}
|
|
377
|
+
catch { }
|
|
378
|
+
try {
|
|
379
|
+
await tempDb.close();
|
|
380
|
+
}
|
|
381
|
+
catch { }
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
else if (conn) {
|
|
385
|
+
// Use existing persistent connection (when called from analyze)
|
|
386
|
+
await conn.query(query);
|
|
387
|
+
return true;
|
|
388
|
+
}
|
|
389
|
+
return false;
|
|
390
|
+
}
|
|
391
|
+
catch (e) {
|
|
392
|
+
// Node may already exist or other error
|
|
393
|
+
console.error(`Failed to insert ${label} node:`, e.message);
|
|
394
|
+
return false;
|
|
395
|
+
}
|
|
396
|
+
};
|
|
397
|
+
/**
|
|
398
|
+
* Batch insert multiple nodes to KuzuDB using a single connection
|
|
399
|
+
* @param nodes - Array of {label, properties} to insert
|
|
400
|
+
* @param dbPath - Path to KuzuDB database
|
|
401
|
+
* @returns Object with success count and error count
|
|
402
|
+
*/
|
|
403
|
+
export const batchInsertNodesToKuzu = async (nodes, dbPath) => {
|
|
404
|
+
if (nodes.length === 0)
|
|
405
|
+
return { inserted: 0, failed: 0 };
|
|
406
|
+
const escapeValue = (v) => {
|
|
407
|
+
if (v === null || v === undefined)
|
|
408
|
+
return 'NULL';
|
|
409
|
+
if (typeof v === 'number')
|
|
410
|
+
return String(v);
|
|
411
|
+
// Escape backslashes first (for Windows paths), then single quotes
|
|
412
|
+
return `'${String(v).replace(/\\/g, '\\\\').replace(/'/g, "''")}'`;
|
|
413
|
+
};
|
|
414
|
+
// Open a single connection for all inserts
|
|
415
|
+
const tempDb = new kuzu.Database(dbPath);
|
|
416
|
+
const tempConn = new kuzu.Connection(tempDb);
|
|
417
|
+
let inserted = 0;
|
|
418
|
+
let failed = 0;
|
|
419
|
+
try {
|
|
420
|
+
for (const { label, properties } of nodes) {
|
|
421
|
+
try {
|
|
422
|
+
let query;
|
|
423
|
+
// Use MERGE instead of CREATE for upsert behavior (handles duplicates gracefully)
|
|
424
|
+
const t = escapeTableName(label);
|
|
425
|
+
if (label === 'File') {
|
|
426
|
+
query = `MERGE (n:File {id: ${escapeValue(properties.id)}}) SET n.name = ${escapeValue(properties.name)}, n.filePath = ${escapeValue(properties.filePath)}, n.content = ${escapeValue(properties.content || '')}`;
|
|
427
|
+
}
|
|
428
|
+
else if (label === 'Folder') {
|
|
429
|
+
query = `MERGE (n:Folder {id: ${escapeValue(properties.id)}}) SET n.name = ${escapeValue(properties.name)}, n.filePath = ${escapeValue(properties.filePath)}`;
|
|
430
|
+
}
|
|
431
|
+
else if (TABLES_WITH_EXPORTED.has(label)) {
|
|
432
|
+
const descPart = properties.description ? `, n.description = ${escapeValue(properties.description)}` : '';
|
|
433
|
+
query = `MERGE (n:${t} {id: ${escapeValue(properties.id)}}) SET n.name = ${escapeValue(properties.name)}, n.filePath = ${escapeValue(properties.filePath)}, n.startLine = ${properties.startLine || 0}, n.endLine = ${properties.endLine || 0}, n.isExported = ${!!properties.isExported}, n.content = ${escapeValue(properties.content || '')}${descPart}`;
|
|
434
|
+
}
|
|
435
|
+
else {
|
|
436
|
+
const descPart = properties.description ? `, n.description = ${escapeValue(properties.description)}` : '';
|
|
437
|
+
query = `MERGE (n:${t} {id: ${escapeValue(properties.id)}}) SET n.name = ${escapeValue(properties.name)}, n.filePath = ${escapeValue(properties.filePath)}, n.startLine = ${properties.startLine || 0}, n.endLine = ${properties.endLine || 0}, n.content = ${escapeValue(properties.content || '')}${descPart}`;
|
|
438
|
+
}
|
|
439
|
+
await tempConn.query(query);
|
|
440
|
+
inserted++;
|
|
441
|
+
}
|
|
442
|
+
catch (e) {
|
|
443
|
+
// Don't console.error here - it corrupts MCP JSON-RPC on stderr
|
|
444
|
+
failed++;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
finally {
|
|
449
|
+
try {
|
|
450
|
+
await tempConn.close();
|
|
451
|
+
}
|
|
452
|
+
catch { }
|
|
453
|
+
try {
|
|
454
|
+
await tempDb.close();
|
|
455
|
+
}
|
|
456
|
+
catch { }
|
|
457
|
+
}
|
|
458
|
+
return { inserted, failed };
|
|
459
|
+
};
|
|
460
|
+
export const executeQuery = async (cypher) => {
|
|
461
|
+
if (!conn) {
|
|
462
|
+
throw new Error('KuzuDB not initialized. Call initKuzu first.');
|
|
463
|
+
}
|
|
464
|
+
const queryResult = await conn.query(cypher);
|
|
465
|
+
// kuzu v0.11 uses getAll() instead of hasNext()/getNext()
|
|
466
|
+
// Query returns QueryResult for single queries, QueryResult[] for multi-statement
|
|
467
|
+
const result = Array.isArray(queryResult) ? queryResult[0] : queryResult;
|
|
468
|
+
const rows = await result.getAll();
|
|
469
|
+
return rows;
|
|
470
|
+
};
|
|
471
|
+
export const executeWithReusedStatement = async (cypher, paramsList) => {
|
|
472
|
+
if (!conn) {
|
|
473
|
+
throw new Error('KuzuDB not initialized. Call initKuzu first.');
|
|
474
|
+
}
|
|
475
|
+
if (paramsList.length === 0)
|
|
476
|
+
return;
|
|
477
|
+
const SUB_BATCH_SIZE = 4;
|
|
478
|
+
for (let i = 0; i < paramsList.length; i += SUB_BATCH_SIZE) {
|
|
479
|
+
const subBatch = paramsList.slice(i, i + SUB_BATCH_SIZE);
|
|
480
|
+
const stmt = await conn.prepare(cypher);
|
|
481
|
+
if (!stmt.isSuccess()) {
|
|
482
|
+
const errMsg = await stmt.getErrorMessage();
|
|
483
|
+
throw new Error(`Prepare failed: ${errMsg}`);
|
|
484
|
+
}
|
|
485
|
+
try {
|
|
486
|
+
for (const params of subBatch) {
|
|
487
|
+
await conn.execute(stmt, params);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
catch (e) {
|
|
491
|
+
// Log the error and continue with next batch
|
|
492
|
+
console.warn('Batch execution error:', e);
|
|
493
|
+
}
|
|
494
|
+
// Note: kuzu 0.8.2 PreparedStatement doesn't require explicit close()
|
|
495
|
+
}
|
|
496
|
+
};
|
|
497
|
+
export const getKuzuStats = async () => {
|
|
498
|
+
if (!conn)
|
|
499
|
+
return { nodes: 0, edges: 0 };
|
|
500
|
+
let totalNodes = 0;
|
|
501
|
+
for (const tableName of NODE_TABLES) {
|
|
502
|
+
try {
|
|
503
|
+
const queryResult = await conn.query(`MATCH (n:${escapeTableName(tableName)}) RETURN count(n) AS cnt`);
|
|
504
|
+
const nodeResult = Array.isArray(queryResult) ? queryResult[0] : queryResult;
|
|
505
|
+
const nodeRows = await nodeResult.getAll();
|
|
506
|
+
if (nodeRows.length > 0) {
|
|
507
|
+
totalNodes += Number(nodeRows[0]?.cnt ?? nodeRows[0]?.[0] ?? 0);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
catch {
|
|
511
|
+
// ignore
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
let totalEdges = 0;
|
|
515
|
+
try {
|
|
516
|
+
const queryResult = await conn.query(`MATCH ()-[r:${REL_TABLE_NAME}]->() RETURN count(r) AS cnt`);
|
|
517
|
+
const edgeResult = Array.isArray(queryResult) ? queryResult[0] : queryResult;
|
|
518
|
+
const edgeRows = await edgeResult.getAll();
|
|
519
|
+
if (edgeRows.length > 0) {
|
|
520
|
+
totalEdges = Number(edgeRows[0]?.cnt ?? edgeRows[0]?.[0] ?? 0);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
catch {
|
|
524
|
+
// ignore
|
|
525
|
+
}
|
|
526
|
+
return { nodes: totalNodes, edges: totalEdges };
|
|
527
|
+
};
|
|
528
|
+
/**
|
|
529
|
+
* Load cached embeddings from KuzuDB before a rebuild.
|
|
530
|
+
* Returns all embedding vectors so they can be re-inserted after the graph is reloaded,
|
|
531
|
+
* avoiding expensive re-embedding of unchanged nodes.
|
|
532
|
+
*/
|
|
533
|
+
export const loadCachedEmbeddings = async () => {
|
|
534
|
+
if (!conn) {
|
|
535
|
+
return { embeddingNodeIds: new Set(), embeddings: [] };
|
|
536
|
+
}
|
|
537
|
+
const embeddingNodeIds = new Set();
|
|
538
|
+
const embeddings = [];
|
|
539
|
+
try {
|
|
540
|
+
const rows = await conn.query(`MATCH (e:${EMBEDDING_TABLE_NAME}) RETURN e.nodeId AS nodeId, e.embedding AS embedding`);
|
|
541
|
+
const result = Array.isArray(rows) ? rows[0] : rows;
|
|
542
|
+
for (const row of await result.getAll()) {
|
|
543
|
+
const nodeId = String(row.nodeId ?? row[0] ?? '');
|
|
544
|
+
if (!nodeId)
|
|
545
|
+
continue;
|
|
546
|
+
embeddingNodeIds.add(nodeId);
|
|
547
|
+
const embedding = row.embedding ?? row[1];
|
|
548
|
+
if (embedding) {
|
|
549
|
+
embeddings.push({
|
|
550
|
+
nodeId,
|
|
551
|
+
embedding: Array.isArray(embedding) ? embedding.map(Number) : Array.from(embedding).map(Number),
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
catch { /* embedding table may not exist */ }
|
|
557
|
+
return { embeddingNodeIds, embeddings };
|
|
558
|
+
};
|
|
559
|
+
export const closeKuzu = async () => {
|
|
560
|
+
if (conn) {
|
|
561
|
+
try {
|
|
562
|
+
await conn.close();
|
|
563
|
+
}
|
|
564
|
+
catch { }
|
|
565
|
+
conn = null;
|
|
566
|
+
}
|
|
567
|
+
if (db) {
|
|
568
|
+
try {
|
|
569
|
+
await db.close();
|
|
570
|
+
}
|
|
571
|
+
catch { }
|
|
572
|
+
db = null;
|
|
573
|
+
}
|
|
574
|
+
currentDbPath = null;
|
|
575
|
+
ftsLoaded = false;
|
|
576
|
+
};
|
|
577
|
+
export const isKuzuReady = () => conn !== null && db !== null;
|
|
578
|
+
/**
|
|
579
|
+
* Delete all nodes (and their relationships) for a specific file from KuzuDB
|
|
580
|
+
* @param filePath - The file path to delete nodes for
|
|
581
|
+
* @param dbPath - Optional path to KuzuDB for per-query connection
|
|
582
|
+
* @returns Object with counts of deleted nodes
|
|
583
|
+
*/
|
|
584
|
+
export const deleteNodesForFile = async (filePath, dbPath) => {
|
|
585
|
+
const usePerQuery = !!dbPath;
|
|
586
|
+
// Set up connection (either use existing or create per-query)
|
|
587
|
+
let tempDb = null;
|
|
588
|
+
let tempConn = null;
|
|
589
|
+
let targetConn = conn;
|
|
590
|
+
if (usePerQuery) {
|
|
591
|
+
tempDb = new kuzu.Database(dbPath);
|
|
592
|
+
tempConn = new kuzu.Connection(tempDb);
|
|
593
|
+
targetConn = tempConn;
|
|
594
|
+
}
|
|
595
|
+
else if (!conn) {
|
|
596
|
+
throw new Error('KuzuDB not initialized. Provide dbPath or call initKuzu first.');
|
|
597
|
+
}
|
|
598
|
+
try {
|
|
599
|
+
let deletedNodes = 0;
|
|
600
|
+
const escapedPath = filePath.replace(/'/g, "''");
|
|
601
|
+
// Delete nodes from each table that has filePath
|
|
602
|
+
// DETACH DELETE removes the node and all its relationships
|
|
603
|
+
for (const tableName of NODE_TABLES) {
|
|
604
|
+
// Skip tables that don't have filePath (Community, Process)
|
|
605
|
+
if (tableName === 'Community' || tableName === 'Process')
|
|
606
|
+
continue;
|
|
607
|
+
try {
|
|
608
|
+
// First count how many we'll delete
|
|
609
|
+
const tn = escapeTableName(tableName);
|
|
610
|
+
const countResult = await targetConn.query(`MATCH (n:${tn}) WHERE n.filePath = '${escapedPath}' RETURN count(n) AS cnt`);
|
|
611
|
+
const result = Array.isArray(countResult) ? countResult[0] : countResult;
|
|
612
|
+
const rows = await result.getAll();
|
|
613
|
+
const count = Number(rows[0]?.cnt ?? rows[0]?.[0] ?? 0);
|
|
614
|
+
if (count > 0) {
|
|
615
|
+
// Delete nodes (and implicitly their relationships via DETACH)
|
|
616
|
+
await targetConn.query(`MATCH (n:${tn}) WHERE n.filePath = '${escapedPath}' DETACH DELETE n`);
|
|
617
|
+
deletedNodes += count;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
catch (e) {
|
|
621
|
+
// Some tables may not support this query, skip
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
// Also delete any embeddings for nodes in this file
|
|
625
|
+
try {
|
|
626
|
+
await targetConn.query(`MATCH (e:${EMBEDDING_TABLE_NAME}) WHERE e.nodeId STARTS WITH '${escapedPath}' DELETE e`);
|
|
627
|
+
}
|
|
628
|
+
catch {
|
|
629
|
+
// Embedding table may not exist or nodeId format may differ
|
|
630
|
+
}
|
|
631
|
+
return { deletedNodes };
|
|
632
|
+
}
|
|
633
|
+
finally {
|
|
634
|
+
// Close per-query connection if used
|
|
635
|
+
if (tempConn) {
|
|
636
|
+
try {
|
|
637
|
+
await tempConn.close();
|
|
638
|
+
}
|
|
639
|
+
catch { }
|
|
640
|
+
}
|
|
641
|
+
if (tempDb) {
|
|
642
|
+
try {
|
|
643
|
+
await tempDb.close();
|
|
644
|
+
}
|
|
645
|
+
catch { }
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
};
|
|
649
|
+
export const getEmbeddingTableName = () => EMBEDDING_TABLE_NAME;
|
|
650
|
+
// ============================================================================
|
|
651
|
+
// Full-Text Search (FTS) Functions
|
|
652
|
+
// ============================================================================
|
|
653
|
+
/**
|
|
654
|
+
* Load the FTS extension (required before using FTS functions).
|
|
655
|
+
* Safe to call multiple times — tracks loaded state via module-level ftsLoaded.
|
|
656
|
+
*/
|
|
657
|
+
export const loadFTSExtension = async () => {
|
|
658
|
+
if (ftsLoaded)
|
|
659
|
+
return;
|
|
660
|
+
if (!conn) {
|
|
661
|
+
throw new Error('KuzuDB not initialized. Call initKuzu first.');
|
|
662
|
+
}
|
|
663
|
+
try {
|
|
664
|
+
await conn.query('INSTALL fts');
|
|
665
|
+
await conn.query('LOAD EXTENSION fts');
|
|
666
|
+
}
|
|
667
|
+
catch {
|
|
668
|
+
// Extension may already be loaded
|
|
669
|
+
}
|
|
670
|
+
ftsLoaded = true;
|
|
671
|
+
};
|
|
672
|
+
/**
|
|
673
|
+
* Create a full-text search index on a table
|
|
674
|
+
* @param tableName - The node table name (e.g., 'File', 'CodeSymbol')
|
|
675
|
+
* @param indexName - Name for the FTS index
|
|
676
|
+
* @param properties - List of properties to index (e.g., ['name', 'code'])
|
|
677
|
+
* @param stemmer - Stemming algorithm (default: 'porter')
|
|
678
|
+
*/
|
|
679
|
+
export const createFTSIndex = async (tableName, indexName, properties, stemmer = 'porter') => {
|
|
680
|
+
if (!conn) {
|
|
681
|
+
throw new Error('KuzuDB not initialized. Call initKuzu first.');
|
|
682
|
+
}
|
|
683
|
+
await loadFTSExtension();
|
|
684
|
+
const propList = properties.map(p => `'${p}'`).join(', ');
|
|
685
|
+
const query = `CALL CREATE_FTS_INDEX('${tableName}', '${indexName}', [${propList}], stemmer := '${stemmer}')`;
|
|
686
|
+
try {
|
|
687
|
+
await conn.query(query);
|
|
688
|
+
}
|
|
689
|
+
catch (e) {
|
|
690
|
+
if (!e.message?.includes('already exists')) {
|
|
691
|
+
throw e;
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
};
|
|
695
|
+
/**
|
|
696
|
+
* Query a full-text search index
|
|
697
|
+
* @param tableName - The node table name
|
|
698
|
+
* @param indexName - FTS index name
|
|
699
|
+
* @param query - Search query string
|
|
700
|
+
* @param limit - Maximum results
|
|
701
|
+
* @param conjunctive - If true, all terms must match (AND); if false, any term matches (OR)
|
|
702
|
+
* @returns Array of { node properties, score }
|
|
703
|
+
*/
|
|
704
|
+
export const queryFTS = async (tableName, indexName, query, limit = 20, conjunctive = false) => {
|
|
705
|
+
if (!conn) {
|
|
706
|
+
throw new Error('KuzuDB not initialized. Call initKuzu first.');
|
|
707
|
+
}
|
|
708
|
+
// Escape single quotes in query
|
|
709
|
+
const escapedQuery = query.replace(/'/g, "''");
|
|
710
|
+
const cypher = `
|
|
711
|
+
CALL QUERY_FTS_INDEX('${tableName}', '${indexName}', '${escapedQuery}', conjunctive := ${conjunctive})
|
|
712
|
+
RETURN node, score
|
|
713
|
+
ORDER BY score DESC
|
|
714
|
+
LIMIT ${limit}
|
|
715
|
+
`;
|
|
716
|
+
try {
|
|
717
|
+
const queryResult = await conn.query(cypher);
|
|
718
|
+
const result = Array.isArray(queryResult) ? queryResult[0] : queryResult;
|
|
719
|
+
const rows = await result.getAll();
|
|
720
|
+
return rows.map((row) => {
|
|
721
|
+
const node = row.node || row[0] || {};
|
|
722
|
+
const score = row.score ?? row[1] ?? 0;
|
|
723
|
+
return {
|
|
724
|
+
nodeId: node.nodeId || node.id || '',
|
|
725
|
+
name: node.name || '',
|
|
726
|
+
filePath: node.filePath || '',
|
|
727
|
+
score: typeof score === 'number' ? score : parseFloat(score) || 0,
|
|
728
|
+
...node,
|
|
729
|
+
};
|
|
730
|
+
});
|
|
731
|
+
}
|
|
732
|
+
catch (e) {
|
|
733
|
+
// Return empty if index doesn't exist yet
|
|
734
|
+
if (e.message?.includes('does not exist')) {
|
|
735
|
+
return [];
|
|
736
|
+
}
|
|
737
|
+
throw e;
|
|
738
|
+
}
|
|
739
|
+
};
|
|
740
|
+
/**
|
|
741
|
+
* Drop an FTS index
|
|
742
|
+
*/
|
|
743
|
+
export const dropFTSIndex = async (tableName, indexName) => {
|
|
744
|
+
if (!conn) {
|
|
745
|
+
throw new Error('KuzuDB not initialized. Call initKuzu first.');
|
|
746
|
+
}
|
|
747
|
+
try {
|
|
748
|
+
await conn.query(`CALL DROP_FTS_INDEX('${tableName}', '${indexName}')`);
|
|
749
|
+
}
|
|
750
|
+
catch {
|
|
751
|
+
// Index may not exist
|
|
752
|
+
}
|
|
753
|
+
};
|