@zuvia-software-solutions/code-mapper 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +215 -0
- package/dist/cli/ai-context.d.ts +19 -0
- package/dist/cli/ai-context.js +168 -0
- package/dist/cli/analyze.d.ts +7 -0
- package/dist/cli/analyze.js +325 -0
- package/dist/cli/augment.d.ts +7 -0
- package/dist/cli/augment.js +27 -0
- package/dist/cli/clean.d.ts +5 -0
- package/dist/cli/clean.js +56 -0
- package/dist/cli/eval-server.d.ts +25 -0
- package/dist/cli/eval-server.js +365 -0
- package/dist/cli/index.d.ts +6 -0
- package/dist/cli/index.js +102 -0
- package/dist/cli/lazy-action.d.ts +6 -0
- package/dist/cli/lazy-action.js +19 -0
- package/dist/cli/list.d.ts +2 -0
- package/dist/cli/list.js +27 -0
- package/dist/cli/mcp.d.ts +8 -0
- package/dist/cli/mcp.js +35 -0
- package/dist/cli/refresh.d.ts +12 -0
- package/dist/cli/refresh.js +165 -0
- package/dist/cli/serve.d.ts +5 -0
- package/dist/cli/serve.js +8 -0
- package/dist/cli/setup.d.ts +6 -0
- package/dist/cli/setup.js +218 -0
- package/dist/cli/status.d.ts +2 -0
- package/dist/cli/status.js +33 -0
- package/dist/cli/tool.d.ts +28 -0
- package/dist/cli/tool.js +87 -0
- package/dist/config/ignore-service.d.ts +32 -0
- package/dist/config/ignore-service.js +282 -0
- package/dist/config/supported-languages.d.ts +23 -0
- package/dist/config/supported-languages.js +52 -0
- package/dist/core/augmentation/engine.d.ts +22 -0
- package/dist/core/augmentation/engine.js +232 -0
- package/dist/core/embeddings/embedder.d.ts +35 -0
- package/dist/core/embeddings/embedder.js +171 -0
- package/dist/core/embeddings/embedding-pipeline.d.ts +41 -0
- package/dist/core/embeddings/embedding-pipeline.js +402 -0
- package/dist/core/embeddings/index.d.ts +5 -0
- package/dist/core/embeddings/index.js +6 -0
- package/dist/core/embeddings/text-generator.d.ts +20 -0
- package/dist/core/embeddings/text-generator.js +159 -0
- package/dist/core/embeddings/types.d.ts +60 -0
- package/dist/core/embeddings/types.js +23 -0
- package/dist/core/graph/graph.d.ts +4 -0
- package/dist/core/graph/graph.js +65 -0
- package/dist/core/graph/types.d.ts +69 -0
- package/dist/core/graph/types.js +3 -0
- package/dist/core/incremental/child-process.d.ts +8 -0
- package/dist/core/incremental/child-process.js +649 -0
- package/dist/core/incremental/refresh-coordinator.d.ts +32 -0
- package/dist/core/incremental/refresh-coordinator.js +147 -0
- package/dist/core/incremental/types.d.ts +78 -0
- package/dist/core/incremental/types.js +153 -0
- package/dist/core/incremental/watcher.d.ts +63 -0
- package/dist/core/incremental/watcher.js +338 -0
- package/dist/core/ingestion/ast-cache.d.ts +12 -0
- package/dist/core/ingestion/ast-cache.js +34 -0
- package/dist/core/ingestion/call-processor.d.ts +34 -0
- package/dist/core/ingestion/call-processor.js +937 -0
- package/dist/core/ingestion/call-routing.d.ts +40 -0
- package/dist/core/ingestion/call-routing.js +97 -0
- package/dist/core/ingestion/cluster-enricher.d.ts +30 -0
- package/dist/core/ingestion/cluster-enricher.js +151 -0
- package/dist/core/ingestion/community-processor.d.ts +26 -0
- package/dist/core/ingestion/community-processor.js +272 -0
- package/dist/core/ingestion/constants.d.ts +5 -0
- package/dist/core/ingestion/constants.js +8 -0
- package/dist/core/ingestion/entry-point-scoring.d.ts +23 -0
- package/dist/core/ingestion/entry-point-scoring.js +317 -0
- package/dist/core/ingestion/export-detection.d.ts +11 -0
- package/dist/core/ingestion/export-detection.js +203 -0
- package/dist/core/ingestion/filesystem-walker.d.ts +18 -0
- package/dist/core/ingestion/filesystem-walker.js +64 -0
- package/dist/core/ingestion/framework-detection.d.ts +42 -0
- package/dist/core/ingestion/framework-detection.js +405 -0
- package/dist/core/ingestion/heritage-processor.d.ts +15 -0
- package/dist/core/ingestion/heritage-processor.js +237 -0
- package/dist/core/ingestion/import-processor.d.ts +31 -0
- package/dist/core/ingestion/import-processor.js +416 -0
- package/dist/core/ingestion/language-config.d.ts +32 -0
- package/dist/core/ingestion/language-config.js +161 -0
- package/dist/core/ingestion/mro-processor.d.ts +32 -0
- package/dist/core/ingestion/mro-processor.js +343 -0
- package/dist/core/ingestion/named-binding-extraction.d.ts +51 -0
- package/dist/core/ingestion/named-binding-extraction.js +343 -0
- package/dist/core/ingestion/parsing-processor.d.ts +20 -0
- package/dist/core/ingestion/parsing-processor.js +282 -0
- package/dist/core/ingestion/pipeline.d.ts +3 -0
- package/dist/core/ingestion/pipeline.js +416 -0
- package/dist/core/ingestion/process-processor.d.ts +42 -0
- package/dist/core/ingestion/process-processor.js +357 -0
- package/dist/core/ingestion/resolution-context.d.ts +40 -0
- package/dist/core/ingestion/resolution-context.js +171 -0
- package/dist/core/ingestion/resolvers/csharp.d.ts +10 -0
- package/dist/core/ingestion/resolvers/csharp.js +101 -0
- package/dist/core/ingestion/resolvers/go.d.ts +8 -0
- package/dist/core/ingestion/resolvers/go.js +33 -0
- package/dist/core/ingestion/resolvers/index.d.ts +14 -0
- package/dist/core/ingestion/resolvers/index.js +10 -0
- package/dist/core/ingestion/resolvers/jvm.d.ts +9 -0
- package/dist/core/ingestion/resolvers/jvm.js +74 -0
- package/dist/core/ingestion/resolvers/php.d.ts +7 -0
- package/dist/core/ingestion/resolvers/php.js +30 -0
- package/dist/core/ingestion/resolvers/ruby.d.ts +9 -0
- package/dist/core/ingestion/resolvers/ruby.js +13 -0
- package/dist/core/ingestion/resolvers/rust.d.ts +5 -0
- package/dist/core/ingestion/resolvers/rust.js +62 -0
- package/dist/core/ingestion/resolvers/standard.d.ts +16 -0
- package/dist/core/ingestion/resolvers/standard.js +144 -0
- package/dist/core/ingestion/resolvers/utils.d.ts +18 -0
- package/dist/core/ingestion/resolvers/utils.js +113 -0
- package/dist/core/ingestion/structure-processor.d.ts +4 -0
- package/dist/core/ingestion/structure-processor.js +39 -0
- package/dist/core/ingestion/symbol-table.d.ts +34 -0
- package/dist/core/ingestion/symbol-table.js +48 -0
- package/dist/core/ingestion/tree-sitter-queries.d.ts +20 -0
- package/dist/core/ingestion/tree-sitter-queries.js +691 -0
- package/dist/core/ingestion/type-env.d.ts +52 -0
- package/dist/core/ingestion/type-env.js +349 -0
- package/dist/core/ingestion/type-extractors/c-cpp.d.ts +4 -0
- package/dist/core/ingestion/type-extractors/c-cpp.js +214 -0
- package/dist/core/ingestion/type-extractors/csharp.d.ts +4 -0
- package/dist/core/ingestion/type-extractors/csharp.js +224 -0
- package/dist/core/ingestion/type-extractors/go.d.ts +4 -0
- package/dist/core/ingestion/type-extractors/go.js +261 -0
- package/dist/core/ingestion/type-extractors/index.d.ts +20 -0
- package/dist/core/ingestion/type-extractors/index.js +30 -0
- package/dist/core/ingestion/type-extractors/jvm.d.ts +5 -0
- package/dist/core/ingestion/type-extractors/jvm.js +386 -0
- package/dist/core/ingestion/type-extractors/php.d.ts +4 -0
- package/dist/core/ingestion/type-extractors/php.js +280 -0
- package/dist/core/ingestion/type-extractors/python.d.ts +4 -0
- package/dist/core/ingestion/type-extractors/python.js +175 -0
- package/dist/core/ingestion/type-extractors/ruby.d.ts +12 -0
- package/dist/core/ingestion/type-extractors/ruby.js +218 -0
- package/dist/core/ingestion/type-extractors/rust.d.ts +4 -0
- package/dist/core/ingestion/type-extractors/rust.js +290 -0
- package/dist/core/ingestion/type-extractors/shared.d.ts +81 -0
- package/dist/core/ingestion/type-extractors/shared.js +322 -0
- package/dist/core/ingestion/type-extractors/swift.d.ts +4 -0
- package/dist/core/ingestion/type-extractors/swift.js +140 -0
- package/dist/core/ingestion/type-extractors/types.d.ts +111 -0
- package/dist/core/ingestion/type-extractors/types.js +4 -0
- package/dist/core/ingestion/type-extractors/typescript.d.ts +4 -0
- package/dist/core/ingestion/type-extractors/typescript.js +227 -0
- package/dist/core/ingestion/utils.d.ts +73 -0
- package/dist/core/ingestion/utils.js +992 -0
- package/dist/core/ingestion/workers/parse-worker.d.ts +99 -0
- package/dist/core/ingestion/workers/parse-worker.js +1055 -0
- package/dist/core/ingestion/workers/worker-pool.d.ts +15 -0
- package/dist/core/ingestion/workers/worker-pool.js +123 -0
- package/dist/core/lbug/csv-generator.d.ts +28 -0
- package/dist/core/lbug/csv-generator.js +355 -0
- package/dist/core/lbug/lbug-adapter.d.ts +96 -0
- package/dist/core/lbug/lbug-adapter.js +753 -0
- package/dist/core/lbug/schema.d.ts +46 -0
- package/dist/core/lbug/schema.js +402 -0
- package/dist/core/search/bm25-index.d.ts +20 -0
- package/dist/core/search/bm25-index.js +123 -0
- package/dist/core/search/hybrid-search.d.ts +32 -0
- package/dist/core/search/hybrid-search.js +131 -0
- package/dist/core/search/query-cache.d.ts +18 -0
- package/dist/core/search/query-cache.js +47 -0
- package/dist/core/search/query-expansion.d.ts +19 -0
- package/dist/core/search/query-expansion.js +75 -0
- package/dist/core/search/reranker.d.ts +29 -0
- package/dist/core/search/reranker.js +122 -0
- package/dist/core/search/types.d.ts +154 -0
- package/dist/core/search/types.js +51 -0
- package/dist/core/semantic/tsgo-service.d.ts +67 -0
- package/dist/core/semantic/tsgo-service.js +355 -0
- package/dist/core/tree-sitter/parser-loader.d.ts +12 -0
- package/dist/core/tree-sitter/parser-loader.js +71 -0
- package/dist/lib/memory-guard.d.ts +35 -0
- package/dist/lib/memory-guard.js +70 -0
- package/dist/lib/utils.d.ts +3 -0
- package/dist/lib/utils.js +6 -0
- package/dist/mcp/compatible-stdio-transport.d.ts +32 -0
- package/dist/mcp/compatible-stdio-transport.js +209 -0
- package/dist/mcp/core/embedder.d.ts +24 -0
- package/dist/mcp/core/embedder.js +168 -0
- package/dist/mcp/core/lbug-adapter.d.ts +29 -0
- package/dist/mcp/core/lbug-adapter.js +330 -0
- package/dist/mcp/local/local-backend.d.ts +188 -0
- package/dist/mcp/local/local-backend.js +2759 -0
- package/dist/mcp/resources.d.ts +22 -0
- package/dist/mcp/resources.js +379 -0
- package/dist/mcp/server.d.ts +10 -0
- package/dist/mcp/server.js +217 -0
- package/dist/mcp/staleness.d.ts +10 -0
- package/dist/mcp/staleness.js +25 -0
- package/dist/mcp/tools.d.ts +21 -0
- package/dist/mcp/tools.js +202 -0
- package/dist/server/api.d.ts +5 -0
- package/dist/server/api.js +340 -0
- package/dist/server/mcp-http.d.ts +7 -0
- package/dist/server/mcp-http.js +95 -0
- package/dist/storage/git.d.ts +6 -0
- package/dist/storage/git.js +35 -0
- package/dist/storage/repo-manager.d.ts +87 -0
- package/dist/storage/repo-manager.js +249 -0
- package/dist/types/pipeline.d.ts +35 -0
- package/dist/types/pipeline.js +20 -0
- package/hooks/claude/code-mapper-hook.cjs +238 -0
- package/hooks/claude/pre-tool-use.sh +79 -0
- package/hooks/claude/session-start.sh +42 -0
- package/models/mlx-embedder.py +185 -0
- package/package.json +100 -0
- package/scripts/patch-tree-sitter-swift.cjs +74 -0
- package/vendor/leiden/index.cjs +355 -0
- package/vendor/leiden/utils.cjs +392 -0
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
// code-mapper/src/mcp/core/lbug-adapter.ts
|
|
2
|
+
/** @file lbug-adapter.ts
|
|
3
|
+
* @description LadybugDB connection pool adapter keyed by repoId
|
|
4
|
+
* Provides checkout/return pool so each concurrent query gets its own Connection
|
|
5
|
+
* from the same Database (Connections are NOT thread-safe)
|
|
6
|
+
* @see https://docs.ladybugdb.com/concurrency */
|
|
7
|
+
import fs from 'fs/promises';
|
|
8
|
+
import lbug from '@ladybugdb/core';
|
|
9
|
+
const pool = new Map();
|
|
10
|
+
const dbCache = new Map();
|
|
11
|
+
/** Max repos in the pool (LRU eviction) */
|
|
12
|
+
const MAX_POOL_SIZE = 5;
|
|
13
|
+
/** Idle timeout before closing a repo's connections */
|
|
14
|
+
const IDLE_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
|
|
15
|
+
/** Max connections per repo (caps concurrent queries per repo) */
|
|
16
|
+
const MAX_CONNS_PER_REPO = 8;
|
|
17
|
+
/** Connections created eagerly on init */
|
|
18
|
+
const INITIAL_CONNS_PER_REPO = 2;
|
|
19
|
+
let idleTimer = null;
|
|
20
|
+
/** Start the idle cleanup timer (runs every 60s) */
|
|
21
|
+
function ensureIdleTimer() {
|
|
22
|
+
if (idleTimer)
|
|
23
|
+
return;
|
|
24
|
+
idleTimer = setInterval(() => {
|
|
25
|
+
const now = Date.now();
|
|
26
|
+
for (const [repoId, entry] of pool) {
|
|
27
|
+
if (now - entry.lastUsed > IDLE_TIMEOUT_MS && entry.checkedOut === 0) {
|
|
28
|
+
closeOne(repoId);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}, 60_000);
|
|
32
|
+
if (idleTimer && typeof idleTimer === 'object' && 'unref' in idleTimer) {
|
|
33
|
+
idleTimer.unref();
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/** Evict the least-recently-used repo if pool is at capacity */
|
|
37
|
+
function evictLRU() {
|
|
38
|
+
if (pool.size < MAX_POOL_SIZE)
|
|
39
|
+
return;
|
|
40
|
+
let oldestId = null;
|
|
41
|
+
let oldestTime = Infinity;
|
|
42
|
+
for (const [id, entry] of pool) {
|
|
43
|
+
if (entry.checkedOut === 0 && entry.lastUsed < oldestTime) {
|
|
44
|
+
oldestTime = entry.lastUsed;
|
|
45
|
+
oldestId = id;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (oldestId) {
|
|
49
|
+
closeOne(oldestId);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Remove a repo from the pool and release its shared Database ref
|
|
54
|
+
* Skips .closeSync() which triggers N-API segfaults on Linux/macOS;
|
|
55
|
+
* pools are read-only so GC reclamation is safe
|
|
56
|
+
*/
|
|
57
|
+
function closeOne(repoId) {
|
|
58
|
+
const entry = pool.get(repoId);
|
|
59
|
+
if (entry) {
|
|
60
|
+
const shared = dbCache.get(entry.dbPath);
|
|
61
|
+
if (shared && shared.refCount > 0) {
|
|
62
|
+
shared.refCount--;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
pool.delete(repoId);
|
|
66
|
+
}
|
|
67
|
+
function createConnection(db) {
|
|
68
|
+
return new lbug.Connection(db);
|
|
69
|
+
}
|
|
70
|
+
/** Query timeout in milliseconds */
|
|
71
|
+
const QUERY_TIMEOUT_MS = 30_000;
|
|
72
|
+
/** Waiter queue timeout in milliseconds */
|
|
73
|
+
const WAITER_TIMEOUT_MS = 15_000;
|
|
74
|
+
const LOCK_RETRY_ATTEMPTS = 3;
|
|
75
|
+
const LOCK_RETRY_DELAY_MS = 2000;
|
|
76
|
+
/**
|
|
77
|
+
* Initialize (or reuse) a Database + connection pool for a specific repo
|
|
78
|
+
* Retries on lock errors (e.g., when `code-mapper analyze` is running)
|
|
79
|
+
*/
|
|
80
|
+
export const initLbug = async (repoId, dbPath) => {
|
|
81
|
+
const existing = pool.get(repoId);
|
|
82
|
+
if (existing) {
|
|
83
|
+
existing.lastUsed = Date.now();
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
// Check if database exists
|
|
87
|
+
try {
|
|
88
|
+
await fs.stat(dbPath);
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
throw new Error(`LadybugDB not found at ${dbPath}. Run: code-mapper analyze`);
|
|
92
|
+
}
|
|
93
|
+
evictLRU();
|
|
94
|
+
// Reuse an existing native Database if another repoId already opened this path
|
|
95
|
+
// Prevents buffer manager exhaustion from multiple mmap regions on the same file
|
|
96
|
+
let shared = dbCache.get(dbPath);
|
|
97
|
+
if (!shared) {
|
|
98
|
+
// Open read-write so incremental refresh can write through the same handle.
|
|
99
|
+
// Lock conflicts with `code-mapper analyze` are handled by retry logic.
|
|
100
|
+
let lastError = null;
|
|
101
|
+
for (let attempt = 1; attempt <= LOCK_RETRY_ATTEMPTS; attempt++) {
|
|
102
|
+
try {
|
|
103
|
+
const db = new lbug.Database(dbPath, 0, // bufferManagerSize (default)
|
|
104
|
+
false, // enableCompression (default)
|
|
105
|
+
false);
|
|
106
|
+
shared = { db, refCount: 0, ftsLoaded: false };
|
|
107
|
+
dbCache.set(dbPath, shared);
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
catch (err) {
|
|
111
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
112
|
+
const isLockError = lastError.message.includes('Could not set lock')
|
|
113
|
+
|| lastError.message.includes('lock');
|
|
114
|
+
if (!isLockError || attempt === LOCK_RETRY_ATTEMPTS)
|
|
115
|
+
break;
|
|
116
|
+
await new Promise(resolve => setTimeout(resolve, LOCK_RETRY_DELAY_MS * attempt));
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (!shared) {
|
|
120
|
+
throw new Error(`LadybugDB unavailable for ${repoId}. Another process may be rebuilding the index. ` +
|
|
121
|
+
`Retry later. (${lastError?.message || 'unknown error'})`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
shared.refCount++;
|
|
125
|
+
const db = shared.db;
|
|
126
|
+
// Pre-create a small pool of connections
|
|
127
|
+
const available = [];
|
|
128
|
+
for (let i = 0; i < INITIAL_CONNS_PER_REPO; i++) {
|
|
129
|
+
available.push(createConnection(db));
|
|
130
|
+
}
|
|
131
|
+
pool.set(repoId, { db, available, checkedOut: 0, waiters: [], lastUsed: Date.now(), dbPath });
|
|
132
|
+
ensureIdleTimer();
|
|
133
|
+
// Load FTS + VECTOR extensions once per shared Database
|
|
134
|
+
if (!shared.ftsLoaded) {
|
|
135
|
+
try {
|
|
136
|
+
await available[0].query('LOAD EXTENSION fts');
|
|
137
|
+
shared.ftsLoaded = true;
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
// Extension may not be installed — FTS queries will fail gracefully
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
// Load VECTOR extension for semantic search (HNSW index queries)
|
|
144
|
+
try {
|
|
145
|
+
await available[0].query('LOAD EXTENSION vector');
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
// Extension may not be installed — vector queries will fall back to BM25-only
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
/**
|
|
152
|
+
* Checkout a connection from the pool
|
|
153
|
+
* Returns an available connection, creates a new one if under cap,
|
|
154
|
+
* or queues the caller until one is returned
|
|
155
|
+
*/
|
|
156
|
+
function checkout(entry) {
|
|
157
|
+
// Fast path: grab an available connection
|
|
158
|
+
if (entry.available.length > 0) {
|
|
159
|
+
entry.checkedOut++;
|
|
160
|
+
return Promise.resolve(entry.available.pop());
|
|
161
|
+
}
|
|
162
|
+
// Grow the pool if under the cap
|
|
163
|
+
const totalConns = entry.available.length + entry.checkedOut;
|
|
164
|
+
if (totalConns < MAX_CONNS_PER_REPO) {
|
|
165
|
+
entry.checkedOut++;
|
|
166
|
+
return Promise.resolve(createConnection(entry.db));
|
|
167
|
+
}
|
|
168
|
+
// At capacity — queue the caller with a timeout.
|
|
169
|
+
return new Promise((resolve, reject) => {
|
|
170
|
+
const waiter = (conn) => {
|
|
171
|
+
clearTimeout(timer);
|
|
172
|
+
resolve(conn);
|
|
173
|
+
};
|
|
174
|
+
const timer = setTimeout(() => {
|
|
175
|
+
const idx = entry.waiters.indexOf(waiter);
|
|
176
|
+
if (idx !== -1)
|
|
177
|
+
entry.waiters.splice(idx, 1);
|
|
178
|
+
reject(new Error(`Connection pool exhausted: timed out after ${WAITER_TIMEOUT_MS}ms waiting for a free connection`));
|
|
179
|
+
}, WAITER_TIMEOUT_MS);
|
|
180
|
+
entry.waiters.push(waiter);
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Return a connection to the pool after use
|
|
185
|
+
* Hands directly to queued waiters to avoid race conditions
|
|
186
|
+
*/
|
|
187
|
+
function checkin(entry, conn) {
|
|
188
|
+
if (entry.waiters.length > 0) {
|
|
189
|
+
// Hand directly to the next waiter (no intermediate available state)
|
|
190
|
+
const waiter = entry.waiters.shift();
|
|
191
|
+
waiter(conn);
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
entry.checkedOut--;
|
|
195
|
+
entry.available.push(conn);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
/** Race a promise against a timeout */
|
|
199
|
+
function withTimeout(promise, ms, label) {
|
|
200
|
+
let timer;
|
|
201
|
+
const timeout = new Promise((_, reject) => {
|
|
202
|
+
timer = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms);
|
|
203
|
+
});
|
|
204
|
+
return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
|
|
205
|
+
}
|
|
206
|
+
export const executeQuery = async (repoId, cypher) => {
|
|
207
|
+
const entry = pool.get(repoId);
|
|
208
|
+
if (!entry) {
|
|
209
|
+
throw new Error(`LadybugDB not initialized for repo "${repoId}". Call initLbug first.`);
|
|
210
|
+
}
|
|
211
|
+
entry.lastUsed = Date.now();
|
|
212
|
+
const conn = await checkout(entry);
|
|
213
|
+
try {
|
|
214
|
+
const queryResult = await withTimeout(conn.query(cypher), QUERY_TIMEOUT_MS, 'Query');
|
|
215
|
+
const result = Array.isArray(queryResult) ? queryResult[0] : queryResult;
|
|
216
|
+
const rows = await result.getAll();
|
|
217
|
+
return rows;
|
|
218
|
+
}
|
|
219
|
+
finally {
|
|
220
|
+
checkin(entry, conn);
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
/** Execute a parameterized query using prepare/execute to prevent Cypher injection */
|
|
224
|
+
export const executeParameterized = async (repoId, cypher, params) => {
|
|
225
|
+
const entry = pool.get(repoId);
|
|
226
|
+
if (!entry) {
|
|
227
|
+
throw new Error(`LadybugDB not initialized for repo "${repoId}". Call initLbug first.`);
|
|
228
|
+
}
|
|
229
|
+
entry.lastUsed = Date.now();
|
|
230
|
+
const conn = await checkout(entry);
|
|
231
|
+
try {
|
|
232
|
+
const stmt = await withTimeout(conn.prepare(cypher), QUERY_TIMEOUT_MS, 'Prepare');
|
|
233
|
+
if (!stmt.isSuccess()) {
|
|
234
|
+
const errMsg = await stmt.getErrorMessage();
|
|
235
|
+
throw new Error(`Prepare failed: ${errMsg}`);
|
|
236
|
+
}
|
|
237
|
+
const queryResult = await withTimeout(conn.execute(stmt, params), QUERY_TIMEOUT_MS, 'Execute');
|
|
238
|
+
const result = Array.isArray(queryResult) ? queryResult[0] : queryResult;
|
|
239
|
+
const rows = await result.getAll();
|
|
240
|
+
return rows;
|
|
241
|
+
}
|
|
242
|
+
finally {
|
|
243
|
+
checkin(entry, conn);
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
/** Close one or all repo pools (provide repoId for single, omit for all) */
|
|
247
|
+
export const closeLbug = async (repoId) => {
|
|
248
|
+
if (repoId) {
|
|
249
|
+
closeOne(repoId);
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
for (const id of [...pool.keys()]) {
|
|
253
|
+
closeOne(id);
|
|
254
|
+
}
|
|
255
|
+
if (idleTimer) {
|
|
256
|
+
clearInterval(idleTimer);
|
|
257
|
+
idleTimer = null;
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
/** Invalidate a repo's pool and shared DB cache, forcing fresh connections on next init.
|
|
261
|
+
* Closes the native Database handle to release the file lock so a child process
|
|
262
|
+
* can open the DB in read-write mode for incremental refresh.
|
|
263
|
+
* Used before AND after the incremental refresh child process writes to the DB. */
|
|
264
|
+
export const invalidateAndReopen = async (repoId) => {
|
|
265
|
+
const entry = pool.get(repoId);
|
|
266
|
+
if (!entry)
|
|
267
|
+
return;
|
|
268
|
+
const entryDbPath = entry.dbPath;
|
|
269
|
+
closeOne(repoId);
|
|
270
|
+
// Close and remove the native Database handle to release the file lock.
|
|
271
|
+
// Without this, the child process cannot acquire a write lock on the DB file.
|
|
272
|
+
const shared = dbCache.get(entryDbPath);
|
|
273
|
+
if (shared && shared.refCount <= 0) {
|
|
274
|
+
try {
|
|
275
|
+
await shared.db.close();
|
|
276
|
+
console.error('Code Mapper: Native DB handle closed successfully');
|
|
277
|
+
}
|
|
278
|
+
catch (err) {
|
|
279
|
+
console.error(`Code Mapper: DB close failed (may still release lock on GC): ${err instanceof Error ? err.message : err}`);
|
|
280
|
+
}
|
|
281
|
+
dbCache.delete(entryDbPath);
|
|
282
|
+
}
|
|
283
|
+
else if (shared) {
|
|
284
|
+
console.error(`Code Mapper: Cannot close DB — ${shared.refCount} other refs still active`);
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
/** Check if a specific repo's pool is active */
|
|
288
|
+
export const isLbugReady = (repoId) => pool.has(repoId);
|
|
289
|
+
/** Write-connection retry config */
|
|
290
|
+
const WRITE_RETRY_ATTEMPTS = 3;
|
|
291
|
+
const WRITE_RETRY_DELAY_MS = 1500;
|
|
292
|
+
/**
|
|
293
|
+
* Open a short-lived read-write connection, execute the callback, then close.
|
|
294
|
+
* The read-only pool must be invalidated BEFORE calling this to avoid lock
|
|
295
|
+
* conflicts. Retries on lock errors (e.g., concurrent `code-mapper analyze`).
|
|
296
|
+
*/
|
|
297
|
+
export const withWriteConnection = async (dbPath, fn) => {
|
|
298
|
+
let lastError = null;
|
|
299
|
+
for (let attempt = 1; attempt <= WRITE_RETRY_ATTEMPTS; attempt++) {
|
|
300
|
+
let db = null;
|
|
301
|
+
let conn = null;
|
|
302
|
+
try {
|
|
303
|
+
db = new lbug.Database(dbPath);
|
|
304
|
+
conn = new lbug.Connection(db);
|
|
305
|
+
const result = await fn(conn);
|
|
306
|
+
return result;
|
|
307
|
+
}
|
|
308
|
+
catch (err) {
|
|
309
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
310
|
+
const isLockError = lastError.message.includes('Could not set lock') ||
|
|
311
|
+
lastError.message.includes('lock');
|
|
312
|
+
if (!isLockError || attempt === WRITE_RETRY_ATTEMPTS)
|
|
313
|
+
break;
|
|
314
|
+
await new Promise((resolve) => setTimeout(resolve, WRITE_RETRY_DELAY_MS * attempt));
|
|
315
|
+
}
|
|
316
|
+
finally {
|
|
317
|
+
try {
|
|
318
|
+
if (conn)
|
|
319
|
+
await conn.close();
|
|
320
|
+
}
|
|
321
|
+
catch { }
|
|
322
|
+
try {
|
|
323
|
+
if (db)
|
|
324
|
+
await db.close();
|
|
325
|
+
}
|
|
326
|
+
catch { }
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
throw lastError ?? new Error('withWriteConnection failed');
|
|
330
|
+
};
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/** @file local-backend.ts
|
|
2
|
+
* @description Tool implementations using local .code-mapper/ indexes
|
|
3
|
+
* Supports multiple indexed repositories via a global registry
|
|
4
|
+
* LadybugDB connections are opened lazily per repo on first query */
|
|
5
|
+
import { type RegistryEntry } from '../../storage/repo-manager.js';
|
|
6
|
+
/** Quick test-file detection for filtering impact results across all supported languages */
|
|
7
|
+
export declare function isTestFilePath(filePath: string): boolean;
|
|
8
|
+
/** Valid LadybugDB node labels for safe Cypher query construction */
|
|
9
|
+
export declare const VALID_NODE_LABELS: Set<string>;
|
|
10
|
+
/** Valid relation types for impact analysis filtering */
|
|
11
|
+
export declare const VALID_RELATION_TYPES: Set<string>;
|
|
12
|
+
/** Regex to detect write operations in user-supplied Cypher queries */
|
|
13
|
+
export declare const CYPHER_WRITE_RE: RegExp;
|
|
14
|
+
/** Check if a Cypher query contains write operations */
|
|
15
|
+
export declare function isWriteQuery(query: string): boolean;
|
|
16
|
+
export interface CodebaseContext {
|
|
17
|
+
projectName: string;
|
|
18
|
+
stats: {
|
|
19
|
+
fileCount: number;
|
|
20
|
+
functionCount: number;
|
|
21
|
+
communityCount: number;
|
|
22
|
+
processCount: number;
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
interface RepoHandle {
|
|
26
|
+
id: string;
|
|
27
|
+
name: string;
|
|
28
|
+
repoPath: string;
|
|
29
|
+
storagePath: string;
|
|
30
|
+
lbugPath: string;
|
|
31
|
+
indexedAt: string;
|
|
32
|
+
lastCommit: string;
|
|
33
|
+
stats?: RegistryEntry['stats'];
|
|
34
|
+
}
|
|
35
|
+
export declare class LocalBackend {
|
|
36
|
+
private repos;
|
|
37
|
+
private contextCache;
|
|
38
|
+
private initializedRepos;
|
|
39
|
+
private watchers;
|
|
40
|
+
/** Per-repo promise chain that serializes ensureFresh calls.
|
|
41
|
+
* Prevents race: Call 2 skipping refresh while Call 1 is still writing. */
|
|
42
|
+
private refreshLocks;
|
|
43
|
+
/** Hard ceiling — beyond this, incremental is unreliable, warn prominently */
|
|
44
|
+
private static readonly MAX_INCREMENTAL_FILES;
|
|
45
|
+
/** Optional tsgo LSP service for confidence-1.0 semantic resolution */
|
|
46
|
+
private tsgoEnabled;
|
|
47
|
+
/** Start file system watcher for a repo to detect source changes */
|
|
48
|
+
private startWatcher;
|
|
49
|
+
/**
|
|
50
|
+
* Seed the watcher with file changes that happened while the MCP server was
|
|
51
|
+
* not running. Compares meta.json lastCommit against current git HEAD and
|
|
52
|
+
* injects any changed files as dirty entries.
|
|
53
|
+
*/
|
|
54
|
+
private seedWatcherFromGit;
|
|
55
|
+
/**
|
|
56
|
+
* Serialized entry point — ensures only one refresh runs per repo at a time.
|
|
57
|
+
* Call 2 waits for Call 1 to finish, then re-checks the watcher.
|
|
58
|
+
*/
|
|
59
|
+
private ensureFresh;
|
|
60
|
+
/** Check for file changes and refresh the DB + embeddings before a tool call */
|
|
61
|
+
private doEnsureFresh;
|
|
62
|
+
/**
|
|
63
|
+
* Tables that are global metadata — NOT deleted per-file during incremental refresh.
|
|
64
|
+
*/
|
|
65
|
+
private static readonly SKIP_DELETE_TABLES;
|
|
66
|
+
/** Tables requiring backtick-quoting in Cypher (reserved words) */
|
|
67
|
+
private static readonly BACKTICK_TABLES;
|
|
68
|
+
private static quoteTable;
|
|
69
|
+
private static escapeCypher;
|
|
70
|
+
/**
|
|
71
|
+
* In-process incremental refresh — parses dirty files with tree-sitter and
|
|
72
|
+
* writes directly to the DB through the existing connection pool.
|
|
73
|
+
*
|
|
74
|
+
* This avoids the LadybugDB lock conflict that prevented the child-process
|
|
75
|
+
* approach from working: LadybugDB on macOS holds an exclusive file lock
|
|
76
|
+
* even for read-only connections, and db.close() segfaults via N-API.
|
|
77
|
+
*/
|
|
78
|
+
private inProcessRefresh;
|
|
79
|
+
/**
|
|
80
|
+
* Update CodeEmbedding rows for dirty files so semantic search is never stale.
|
|
81
|
+
*
|
|
82
|
+
* Runs ONLY when the repo previously had embeddings (stats.embeddings > 0).
|
|
83
|
+
* Steps:
|
|
84
|
+
* 1. Delete stale CodeEmbedding rows for all dirty file paths (always)
|
|
85
|
+
* 2. Query new embeddable nodes for modified/created files
|
|
86
|
+
* 3. Generate text → batch embed using the warm MCP singleton model
|
|
87
|
+
* 4. Insert new CodeEmbedding rows
|
|
88
|
+
* 5. Drop + recreate HNSW vector index
|
|
89
|
+
*
|
|
90
|
+
* If the embedding model fails to load, stale rows are still deleted —
|
|
91
|
+
* semantic search returns fewer results but never wrong ones.
|
|
92
|
+
*/
|
|
93
|
+
private refreshEmbeddings;
|
|
94
|
+
private rebuildVectorIndex;
|
|
95
|
+
/**
|
|
96
|
+
* Initialize from the global registry, returns true if at least one repo is available.
|
|
97
|
+
* @param opts.tsgo — Enable tsgo semantic resolution (confidence-1.0 call edges)
|
|
98
|
+
*/
|
|
99
|
+
init(opts?: {
|
|
100
|
+
tsgo?: boolean;
|
|
101
|
+
}): Promise<boolean>;
|
|
102
|
+
/**
|
|
103
|
+
* Re-read the global registry and update the in-memory repo map
|
|
104
|
+
* LadybugDB connections for removed repos idle-timeout naturally
|
|
105
|
+
*/
|
|
106
|
+
private refreshRepos;
|
|
107
|
+
/** Generate a stable repo ID from name + path (appends hash on collision) */
|
|
108
|
+
private repoId;
|
|
109
|
+
/**
|
|
110
|
+
* Resolve which repo to use (by name, path, or single-repo default)
|
|
111
|
+
* Re-reads the registry once on miss in case a new repo was indexed
|
|
112
|
+
*/
|
|
113
|
+
resolveRepo(repoParam?: string): Promise<RepoHandle>;
|
|
114
|
+
/** Try to resolve a repo from the in-memory cache, returns null on miss */
|
|
115
|
+
private resolveRepoFromCache;
|
|
116
|
+
private ensureInitialized;
|
|
117
|
+
/** Get context for a specific repo (or the single repo if only one) */
|
|
118
|
+
getContext(repoId?: string): CodebaseContext | null;
|
|
119
|
+
/** List all registered repos, re-reading the registry to discover new ones */
|
|
120
|
+
listRepos(): Promise<Array<{
|
|
121
|
+
name: string;
|
|
122
|
+
path: string;
|
|
123
|
+
indexedAt: string;
|
|
124
|
+
lastCommit: string;
|
|
125
|
+
stats?: any;
|
|
126
|
+
}>>;
|
|
127
|
+
/** Extract signature from content: the declaration line(s), not the full body */
|
|
128
|
+
private extractSignature;
|
|
129
|
+
/** Short file path: strip common prefix if all paths share it */
|
|
130
|
+
private shortPath;
|
|
131
|
+
/** C2: Count how many initial steps two flows share (by symbol name) */
|
|
132
|
+
private sharedPrefixLength;
|
|
133
|
+
/** D1: Generate a readable flow description from step names */
|
|
134
|
+
/** Generate readable flow description from step names.
|
|
135
|
+
* Uses step 2 (first dispatch point) as the middle — it's where the entry
|
|
136
|
+
* point specializes and is the most discriminating step in most flows. */
|
|
137
|
+
private describeFlow;
|
|
138
|
+
private formatQueryAsText;
|
|
139
|
+
private formatContextAsText;
|
|
140
|
+
private formatImpactAsText;
|
|
141
|
+
private formatDetectChangesAsText;
|
|
142
|
+
/** C3: Check if index is behind HEAD and return a warning prefix */
|
|
143
|
+
private getStalenessWarning;
|
|
144
|
+
callTool(method: string, params: any): Promise<any>;
|
|
145
|
+
/**
|
|
146
|
+
* Query tool: process-grouped search
|
|
147
|
+
* Hybrid BM25+semantic search, trace to processes, rank by relevance + cohesion
|
|
148
|
+
*/
|
|
149
|
+
private query;
|
|
150
|
+
/**
|
|
151
|
+
* BM25 keyword search helper - uses LadybugDB FTS for always-fresh results
|
|
152
|
+
*/
|
|
153
|
+
private bm25Search;
|
|
154
|
+
/**
|
|
155
|
+
* Semantic vector search helper
|
|
156
|
+
*/
|
|
157
|
+
private semanticSearch;
|
|
158
|
+
executeCypher(repoName: string, query: string): Promise<any>;
|
|
159
|
+
private cypher;
|
|
160
|
+
/** Format raw Cypher result rows as a markdown table, with raw fallback */
|
|
161
|
+
private formatCypherAsMarkdown;
|
|
162
|
+
/** Aggregate same-named clusters by heuristicLabel, filtering tiny clusters (<5 symbols) */
|
|
163
|
+
private aggregateClusters;
|
|
164
|
+
private overview;
|
|
165
|
+
/** Context tool: 360-degree symbol view with categorized refs and disambiguation */
|
|
166
|
+
private context;
|
|
167
|
+
/** Legacy explore for backwards compatibility with resources.ts */
|
|
168
|
+
private explore;
|
|
169
|
+
/** Detect changes: git-diff impact analysis mapping changed lines to symbols and processes */
|
|
170
|
+
private detectChanges;
|
|
171
|
+
/** Rename tool: multi-file coordinated rename using graph (high confidence) + text search */
|
|
172
|
+
private rename;
|
|
173
|
+
private impact;
|
|
174
|
+
/** Query clusters (communities) directly from graph for getClustersResource */
|
|
175
|
+
queryClusters(repoName?: string, limit?: number): Promise<{
|
|
176
|
+
clusters: any[];
|
|
177
|
+
}>;
|
|
178
|
+
/** Query processes directly from graph for getProcessesResource */
|
|
179
|
+
queryProcesses(repoName?: string, limit?: number): Promise<{
|
|
180
|
+
processes: any[];
|
|
181
|
+
}>;
|
|
182
|
+
/** Query cluster detail (members) for getClusterDetailResource */
|
|
183
|
+
queryClusterDetail(name: string, repoName?: string): Promise<any>;
|
|
184
|
+
/** Query process detail (steps) for getProcessDetailResource */
|
|
185
|
+
queryProcessDetail(name: string, repoName?: string): Promise<any>;
|
|
186
|
+
disconnect(): Promise<void>;
|
|
187
|
+
}
|
|
188
|
+
export {};
|