ultracode 5.3.0 → 5.5.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/dist/chunks/analysis-tool-handlers-H2RXLDPX.js +817 -0
- package/dist/chunks/analysis-tool-handlers-RJZAR6VT.js +817 -0
- package/dist/chunks/analysis-tool-handlers-Z2RF24T7.js +13 -0
- package/dist/chunks/autodoc-tool-handlers-CV5JEQUA.js +1112 -0
- package/dist/chunks/autodoc-tool-handlers-EHTNCH6I.js +1112 -0
- package/dist/chunks/autodoc-tool-handlers-MECXQJ2K.js +138 -0
- package/dist/chunks/chaos-CO7TOBOJ.js +18 -0
- package/dist/chunks/chaos-VM2PXERO.js +1573 -0
- package/dist/chunks/chaos-W3XRVJ7K.js +1564 -0
- package/dist/chunks/chunk-6K37BWK5.js +439 -0
- package/dist/chunks/chunk-EALTCYHZ.js +10 -0
- package/dist/chunks/chunk-FTBE7VMY.js +316 -0
- package/dist/chunks/chunk-KBW6LRQP.js +322 -0
- package/dist/chunks/chunk-NKUHX4CU.js +5 -0
- package/dist/chunks/chunk-NZFF4DQ4.js +3179 -0
- package/dist/chunks/chunk-RGP5UVQ7.js +3179 -0
- package/dist/chunks/chunk-RMZXFGQZ.js +322 -0
- package/dist/chunks/chunk-UG44F23Y.js +316 -0
- package/dist/chunks/chunk-V2SCB5H5.js +4403 -0
- package/dist/chunks/chunk-V6JAQNM3.js +1 -0
- package/dist/chunks/chunk-XFGXM4CR.js +4403 -0
- package/dist/chunks/dev-agent-JVIGBMHQ.js +1 -0
- package/dist/chunks/dev-agent-TRVP5U6N.js +1624 -0
- package/dist/chunks/dev-agent-Y5G5WKQ4.js +1624 -0
- package/dist/chunks/graph-storage-factory-AYZ57YSL.js +13 -0
- package/dist/chunks/graph-storage-factory-GTAIJEI5.js +1 -0
- package/dist/chunks/graph-storage-factory-T2WO5QVG.js +13 -0
- package/dist/chunks/incremental-updater-KDIQGAUU.js +14 -0
- package/dist/chunks/incremental-updater-OJRSTO3Q.js +1 -0
- package/dist/chunks/incremental-updater-SBEBH7KF.js +14 -0
- package/dist/chunks/indexer-agent-H3QIEL3Z.js +21 -0
- package/dist/chunks/indexer-agent-KHF5JMV7.js +21 -0
- package/dist/chunks/indexer-agent-SHJD6Z77.js +1 -0
- package/dist/chunks/indexing-pipeline-J6Z4BHKF.js +1 -0
- package/dist/chunks/indexing-pipeline-OY3337QN.js +249 -0
- package/dist/chunks/indexing-pipeline-WCXIDMAP.js +249 -0
- package/dist/chunks/merge-agent-LSUBDJB2.js +2481 -0
- package/dist/chunks/merge-agent-MJEW3HWU.js +2481 -0
- package/dist/chunks/merge-agent-O45OXF33.js +11 -0
- package/dist/chunks/merge-tool-handlers-BDSVNQVZ.js +277 -0
- package/dist/chunks/merge-tool-handlers-HP7DRBXJ.js +1 -0
- package/dist/chunks/merge-tool-handlers-RUJAKE3D.js +277 -0
- package/dist/chunks/pattern-tool-handlers-L62W3CXR.js +1549 -0
- package/dist/chunks/pattern-tool-handlers-SAHX2CVW.js +13 -0
- package/dist/chunks/query-agent-3TWDFIMT.js +191 -0
- package/dist/chunks/query-agent-HXQ3BMMF.js +191 -0
- package/dist/chunks/query-agent-USMC2GNG.js +1 -0
- package/dist/chunks/semantic-agent-MQCAWIAB.js +6381 -0
- package/dist/chunks/semantic-agent-NDGR3NAK.js +6381 -0
- package/dist/chunks/semantic-agent-S4ZL6GZC.js +137 -0
- package/dist/index.js +17 -17
- package/dist/roslyn-addon/.build-hash +1 -1
- package/dist/roslyn-addon/ILGPU.Algorithms.dll +0 -0
- package/dist/roslyn-addon/ILGPU.dll +0 -0
- package/dist/roslyn-addon/UltraCode.CSharp.deps.json +35 -0
- package/dist/roslyn-addon/UltraCode.CSharp.dll +0 -0
- package/package.json +1 -1
|
@@ -0,0 +1,3179 @@
|
|
|
1
|
+
import { knowledgeBus } from './chunk-JPI46FLQ.js';
|
|
2
|
+
import { BaseAgent } from './chunk-IGUCJL3R.js';
|
|
3
|
+
import { getGraphStorage, getLibSQLAdapter } from './chunk-XFGXM4CR.js';
|
|
4
|
+
import { flattenParsedEntities, parsedEntityToEntity } from './chunk-CTXFPNDA.js';
|
|
5
|
+
import { init_storage_paths, getDataDir, getProjectHash, getCurrentGitBranchOrDefault } from './chunk-ZD54CMKT.js';
|
|
6
|
+
import { init_fast_hash, hashText } from './chunk-HEMJHRHZ.js';
|
|
7
|
+
import { sleep, tryGarbageCollect } from './chunk-IMQ6WSJV.js';
|
|
8
|
+
import { init_yaml_config, getConfig } from './chunk-XV6GNSLC.js';
|
|
9
|
+
import { init_file_ops, existsSync, readTextSync, mkdirSync, writeFileSync, readdirSync, statSync, unlinkSync } from './chunk-F7CKCMXI.js';
|
|
10
|
+
import { init_logging, log, logMemory } from './chunk-VCCBEJQ5.js';
|
|
11
|
+
import { nanoid } from 'nanoid';
|
|
12
|
+
import { execSync } from 'child_process';
|
|
13
|
+
import { join, isAbsolute, dirname } from 'path';
|
|
14
|
+
import { EventEmitter } from 'events';
|
|
15
|
+
import { existsSync as existsSync$1, watch } from 'fs';
|
|
16
|
+
import { stat } from 'fs/promises';
|
|
17
|
+
import fg from 'fast-glob';
|
|
18
|
+
import xxhash from 'xxhash-wasm';
|
|
19
|
+
import { LRUCache } from 'lru-cache';
|
|
20
|
+
|
|
21
|
+
// src/agents/indexer-agent.ts
|
|
22
|
+
init_yaml_config();
|
|
23
|
+
|
|
24
|
+
// src/core/branch-manager.ts
|
|
25
|
+
init_logging();
|
|
26
|
+
init_storage_paths();
|
|
27
|
+
init_file_ops();
|
|
28
|
+
var BranchManager = class {
|
|
29
|
+
config;
|
|
30
|
+
registryPath;
|
|
31
|
+
registry;
|
|
32
|
+
currentBranch = null;
|
|
33
|
+
currentRepoPath = null;
|
|
34
|
+
constructor(config) {
|
|
35
|
+
this.config = config;
|
|
36
|
+
this.registryPath = join(config.dataDir, "branch-registry.json");
|
|
37
|
+
this.registry = this.loadRegistry();
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Get projects directory (centralized storage for all repos/branches)
|
|
41
|
+
*/
|
|
42
|
+
getProjectsDir() {
|
|
43
|
+
return join(this.config.dataDir, "projects");
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Initialize the branch manager (no-op, kept for API compatibility)
|
|
47
|
+
*/
|
|
48
|
+
async initialize() {
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Get current Git branch name
|
|
52
|
+
*/
|
|
53
|
+
getCurrentBranch(repoPath) {
|
|
54
|
+
const path = repoPath || this.currentRepoPath || process.cwd();
|
|
55
|
+
try {
|
|
56
|
+
const gitDir = join(path, ".git");
|
|
57
|
+
if (!existsSync(gitDir)) {
|
|
58
|
+
log.d("BRANCHMGR", "no_git_dir", { path });
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
const branch = execSync("git symbolic-ref --short HEAD", {
|
|
63
|
+
cwd: path,
|
|
64
|
+
encoding: "utf-8",
|
|
65
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
66
|
+
windowsHide: true
|
|
67
|
+
}).trim();
|
|
68
|
+
this.currentBranch = branch;
|
|
69
|
+
this.currentRepoPath = path;
|
|
70
|
+
return branch;
|
|
71
|
+
} catch {
|
|
72
|
+
try {
|
|
73
|
+
const describe = execSync("git describe --tags --exact-match", {
|
|
74
|
+
cwd: path,
|
|
75
|
+
encoding: "utf-8",
|
|
76
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
77
|
+
windowsHide: true
|
|
78
|
+
}).trim();
|
|
79
|
+
this.currentBranch = `detached-${describe}`;
|
|
80
|
+
this.currentRepoPath = path;
|
|
81
|
+
return this.currentBranch;
|
|
82
|
+
} catch {
|
|
83
|
+
const hash = execSync("git rev-parse --short HEAD", {
|
|
84
|
+
cwd: path,
|
|
85
|
+
encoding: "utf-8",
|
|
86
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
87
|
+
windowsHide: true
|
|
88
|
+
}).trim();
|
|
89
|
+
this.currentBranch = `detached-${hash}`;
|
|
90
|
+
this.currentRepoPath = path;
|
|
91
|
+
return this.currentBranch;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
} catch (error) {
|
|
95
|
+
log.d("BRANCHMGR", "branch_get_fail", { err: String(error) });
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Get last commit hash for current branch
|
|
101
|
+
*/
|
|
102
|
+
getLastCommitHash(repoPath) {
|
|
103
|
+
const path = repoPath || this.currentRepoPath || process.cwd();
|
|
104
|
+
try {
|
|
105
|
+
const hash = execSync("git rev-parse HEAD", {
|
|
106
|
+
cwd: path,
|
|
107
|
+
encoding: "utf-8",
|
|
108
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
109
|
+
windowsHide: true
|
|
110
|
+
}).trim();
|
|
111
|
+
return hash;
|
|
112
|
+
} catch (error) {
|
|
113
|
+
log.d("BRANCHMGR", "commit_hash_fail", { err: String(error) });
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Get repository hash for path isolation
|
|
119
|
+
* Uses the same hash as storage-paths.ts for consistency
|
|
120
|
+
*/
|
|
121
|
+
getRepositoryHash(repoPath) {
|
|
122
|
+
return getProjectHash(repoPath);
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Sanitize branch name for filesystem
|
|
126
|
+
*/
|
|
127
|
+
sanitizeBranchName(branch) {
|
|
128
|
+
return branch.replace(/\//g, "-").replace(/\\/g, "-").replace(/[^a-zA-Z0-9_-]/g, "_").replace(/^-+|-+$/g, "");
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Get database path for a specific branch
|
|
132
|
+
* Returns the unified storage database path (same for all branches)
|
|
133
|
+
*/
|
|
134
|
+
getBranchDbPath(_branch, _repoPath) {
|
|
135
|
+
return join(this.config.dataDir, "unified-storage.db");
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Get FAISS index path for a specific branch
|
|
139
|
+
*/
|
|
140
|
+
getFaissIndexPath(branch, repoPath) {
|
|
141
|
+
const path = repoPath || this.currentRepoPath || process.cwd();
|
|
142
|
+
const repoHash = this.getRepositoryHash(path);
|
|
143
|
+
const sanitizedBranch = this.sanitizeBranchName(branch);
|
|
144
|
+
return join(this.getProjectsDir(), repoHash, `faiss-${sanitizedBranch}.bin`);
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Get metadata for a branch
|
|
148
|
+
*/
|
|
149
|
+
getBranchMetadata(branch, repoPath) {
|
|
150
|
+
const path = repoPath || this.currentRepoPath || process.cwd();
|
|
151
|
+
const repoHash = this.getRepositoryHash(path);
|
|
152
|
+
const sanitizedBranch = this.sanitizeBranchName(branch);
|
|
153
|
+
const metadataPath = join(this.getProjectsDir(), repoHash, sanitizedBranch, "metadata.json");
|
|
154
|
+
if (!existsSync(metadataPath)) {
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
try {
|
|
158
|
+
const data = readTextSync(metadataPath);
|
|
159
|
+
return JSON.parse(data);
|
|
160
|
+
} catch (error) {
|
|
161
|
+
log.e("BRANCHMGR", "metadata_read_fail", { err: String(error) });
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Update branch metadata
|
|
167
|
+
*/
|
|
168
|
+
updateBranchMetadata(metadata) {
|
|
169
|
+
const sanitizedBranch = this.sanitizeBranchName(metadata.branch);
|
|
170
|
+
const metadataPath = join(this.getProjectsDir(), metadata.repositoryHash, sanitizedBranch, "metadata.json");
|
|
171
|
+
const dir = join(this.getProjectsDir(), metadata.repositoryHash, sanitizedBranch);
|
|
172
|
+
if (!existsSync(dir)) {
|
|
173
|
+
mkdirSync(dir, { recursive: true });
|
|
174
|
+
}
|
|
175
|
+
writeFileSync(metadataPath, JSON.stringify(metadata, null, 2), "utf-8");
|
|
176
|
+
this.updateRegistry(metadata.repositoryPath, metadata.branch, metadataPath);
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Get all indexed branches for a repository
|
|
180
|
+
* Detects branches by FAISS index files (faiss-{branch}.bin)
|
|
181
|
+
*/
|
|
182
|
+
getActiveBranches(repoPath) {
|
|
183
|
+
const path = repoPath || this.currentRepoPath || process.cwd();
|
|
184
|
+
const repoHash = this.getRepositoryHash(path);
|
|
185
|
+
const repoDir = join(this.getProjectsDir(), repoHash);
|
|
186
|
+
if (!existsSync(repoDir)) {
|
|
187
|
+
return [];
|
|
188
|
+
}
|
|
189
|
+
const branches = [];
|
|
190
|
+
const files = readdirSync(repoDir);
|
|
191
|
+
const faissPattern = /^faiss-(.+)\.bin$/;
|
|
192
|
+
for (const file of files) {
|
|
193
|
+
const match = file.match(faissPattern);
|
|
194
|
+
if (!match || !match[1]) continue;
|
|
195
|
+
const branchName = match[1];
|
|
196
|
+
const faissPath = join(repoDir, file);
|
|
197
|
+
const stats = statSync(faissPath);
|
|
198
|
+
const metadata = this.getBranchMetadata(branchName, path);
|
|
199
|
+
branches.push({
|
|
200
|
+
name: branchName,
|
|
201
|
+
dbPath: faissPath,
|
|
202
|
+
lastAccessed: metadata?.accessedAt || stats.mtimeMs,
|
|
203
|
+
sizeBytes: stats.size,
|
|
204
|
+
metadata
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
return branches.sort((a, b) => b.lastAccessed - a.lastAccessed);
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Cleanup old branches using LRU eviction
|
|
211
|
+
*/
|
|
212
|
+
async cleanupOldBranches(keep) {
|
|
213
|
+
const keepCount = keep || this.config.maxBranchesPerRepo;
|
|
214
|
+
const branches = this.getActiveBranches();
|
|
215
|
+
if (branches.length <= keepCount) {
|
|
216
|
+
return 0;
|
|
217
|
+
}
|
|
218
|
+
const toDelete = branches.slice(keepCount);
|
|
219
|
+
let deletedCount = 0;
|
|
220
|
+
for (const branch of toDelete) {
|
|
221
|
+
try {
|
|
222
|
+
const branchDir = join(this.getProjectsDir(), branch.metadata?.repositoryHash || "", branch.name);
|
|
223
|
+
const files = readdirSync(branchDir);
|
|
224
|
+
for (const file of files) {
|
|
225
|
+
unlinkSync(join(branchDir, file));
|
|
226
|
+
}
|
|
227
|
+
if (branch.metadata) {
|
|
228
|
+
this.removeFromRegistry(branch.metadata.repositoryPath, branch.name);
|
|
229
|
+
}
|
|
230
|
+
deletedCount++;
|
|
231
|
+
log.i("BRANCHMGR", "branch_cleaned", { branch: branch.name });
|
|
232
|
+
} catch (error) {
|
|
233
|
+
log.e("BRANCHMGR", "cleanup_fail", { branch: branch.name, err: String(error) });
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return deletedCount;
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Switch to a different branch
|
|
240
|
+
*/
|
|
241
|
+
async switchBranch(newBranch, repoPath) {
|
|
242
|
+
const path = repoPath || this.currentRepoPath || process.cwd();
|
|
243
|
+
const oldBranch = this.currentBranch;
|
|
244
|
+
this.currentBranch = newBranch;
|
|
245
|
+
this.currentRepoPath = path;
|
|
246
|
+
log.i("BRANCHMGR", "branch_switched", { from: oldBranch, to: newBranch });
|
|
247
|
+
const metadata = this.getBranchMetadata(newBranch, path);
|
|
248
|
+
if (metadata) {
|
|
249
|
+
metadata.accessedAt = Date.now();
|
|
250
|
+
this.updateBranchMetadata(metadata);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Check if branch has been indexed (FAISS index exists)
|
|
255
|
+
*/
|
|
256
|
+
hasBranchDatabase(branch, repoPath) {
|
|
257
|
+
const faissPath = this.getFaissIndexPath(branch, repoPath);
|
|
258
|
+
return existsSync(faissPath);
|
|
259
|
+
}
|
|
260
|
+
// =============================================================================
|
|
261
|
+
// PRIVATE METHODS - Registry Management
|
|
262
|
+
// =============================================================================
|
|
263
|
+
loadRegistry() {
|
|
264
|
+
if (!existsSync(this.registryPath)) {
|
|
265
|
+
return {
|
|
266
|
+
repositories: {},
|
|
267
|
+
config: {
|
|
268
|
+
maxBranchesPerRepo: this.config.maxBranchesPerRepo,
|
|
269
|
+
maxTotalBranches: this.config.maxTotalBranches,
|
|
270
|
+
evictionStrategy: this.config.evictionStrategy
|
|
271
|
+
}
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
try {
|
|
275
|
+
const data = readTextSync(this.registryPath);
|
|
276
|
+
return JSON.parse(data);
|
|
277
|
+
} catch (error) {
|
|
278
|
+
log.e("BRANCHMGR", "registry_load_fail", { err: String(error) });
|
|
279
|
+
return {
|
|
280
|
+
repositories: {},
|
|
281
|
+
config: {
|
|
282
|
+
maxBranchesPerRepo: this.config.maxBranchesPerRepo,
|
|
283
|
+
maxTotalBranches: this.config.maxTotalBranches,
|
|
284
|
+
evictionStrategy: this.config.evictionStrategy
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
saveRegistry() {
|
|
290
|
+
const dir = join(this.config.dataDir);
|
|
291
|
+
if (!existsSync(dir)) {
|
|
292
|
+
mkdirSync(dir, { recursive: true });
|
|
293
|
+
}
|
|
294
|
+
writeFileSync(this.registryPath, JSON.stringify(this.registry, null, 2), "utf-8");
|
|
295
|
+
}
|
|
296
|
+
updateRegistry(repoPath, branch, dbPath) {
|
|
297
|
+
const repoHash = this.getRepositoryHash(repoPath);
|
|
298
|
+
if (!this.registry.repositories[repoHash]) {
|
|
299
|
+
this.registry.repositories[repoHash] = {
|
|
300
|
+
path: repoPath,
|
|
301
|
+
branches: {}
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
const sanitizedBranch = this.sanitizeBranchName(branch);
|
|
305
|
+
const stats = existsSync(dbPath) ? statSync(dbPath) : { size: 0};
|
|
306
|
+
this.registry.repositories[repoHash].branches[sanitizedBranch] = {
|
|
307
|
+
dbPath,
|
|
308
|
+
lastAccessed: Date.now(),
|
|
309
|
+
sizeBytes: stats.size
|
|
310
|
+
};
|
|
311
|
+
this.saveRegistry();
|
|
312
|
+
}
|
|
313
|
+
removeFromRegistry(repoPath, branch) {
|
|
314
|
+
const repoHash = this.getRepositoryHash(repoPath);
|
|
315
|
+
const sanitizedBranch = this.sanitizeBranchName(branch);
|
|
316
|
+
if (this.registry.repositories[repoHash]?.branches[sanitizedBranch]) {
|
|
317
|
+
delete this.registry.repositories[repoHash].branches[sanitizedBranch];
|
|
318
|
+
this.saveRegistry();
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
// src/core/file-watcher.ts
|
|
324
|
+
init_logging();
|
|
325
|
+
var isBunRuntime = typeof globalThis.Bun !== "undefined";
|
|
326
|
+
var hasBunWatch = isBunRuntime && typeof globalThis.Bun !== "undefined" && typeof globalThis.Bun["watch"] === "function";
|
|
327
|
+
var FileWatcher = class extends EventEmitter {
|
|
328
|
+
config;
|
|
329
|
+
watchers = /* @__PURE__ */ new Map();
|
|
330
|
+
watchedFiles = /* @__PURE__ */ new Set();
|
|
331
|
+
isRunning = false;
|
|
332
|
+
// Debouncing
|
|
333
|
+
pendingChanges = /* @__PURE__ */ new Map();
|
|
334
|
+
debounceAbort = null;
|
|
335
|
+
// Bun watcher (when available)
|
|
336
|
+
bunWatcher = null;
|
|
337
|
+
constructor(config) {
|
|
338
|
+
super();
|
|
339
|
+
this.config = {
|
|
340
|
+
rootDir: config.rootDir.replace(/\\/g, "/"),
|
|
341
|
+
include: config.include ?? ["**/*"],
|
|
342
|
+
exclude: config.exclude ?? ["**/node_modules/**", "**/.git/**", "**/dist/**", "**/build/**", "**/.ultracode/**"],
|
|
343
|
+
debounceMs: config.debounceMs ?? 100,
|
|
344
|
+
bulkThreshold: config.bulkThreshold ?? 1e3
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* Start watching
|
|
349
|
+
*/
|
|
350
|
+
async start() {
|
|
351
|
+
if (this.isRunning) return;
|
|
352
|
+
this.isRunning = true;
|
|
353
|
+
const runtime = hasBunWatch ? "Bun.watch" : "fs.watch";
|
|
354
|
+
log.i("FILEWATCHER", "Starting", { runtime, rootDir: this.config.rootDir, isBun: isBunRuntime, hasBunWatch });
|
|
355
|
+
try {
|
|
356
|
+
const files = await this.scanFiles();
|
|
357
|
+
log.d("FILEWATCHER", "Initial scan complete", { files: files.length });
|
|
358
|
+
if (hasBunWatch) {
|
|
359
|
+
await this.startBunWatcher(files);
|
|
360
|
+
} else {
|
|
361
|
+
await this.startNodeWatcher(files);
|
|
362
|
+
}
|
|
363
|
+
this.emit("ready");
|
|
364
|
+
} catch (error) {
|
|
365
|
+
this.isRunning = false;
|
|
366
|
+
log.e("FILEWATCHER", "Failed to start", { error: error.message });
|
|
367
|
+
throw error;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* Stop watching
|
|
372
|
+
*/
|
|
373
|
+
async stop() {
|
|
374
|
+
if (!this.isRunning) return;
|
|
375
|
+
this.isRunning = false;
|
|
376
|
+
if (this.debounceAbort) {
|
|
377
|
+
this.debounceAbort.abort();
|
|
378
|
+
this.debounceAbort = null;
|
|
379
|
+
}
|
|
380
|
+
if (this.bunWatcher) {
|
|
381
|
+
this.bunWatcher.stop();
|
|
382
|
+
this.bunWatcher = null;
|
|
383
|
+
}
|
|
384
|
+
for (const watcher of this.watchers.values()) {
|
|
385
|
+
watcher.close();
|
|
386
|
+
}
|
|
387
|
+
this.watchers.clear();
|
|
388
|
+
this.watchedFiles.clear();
|
|
389
|
+
this.pendingChanges.clear();
|
|
390
|
+
log.d("FILEWATCHER", "Stopped");
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* Scan files matching patterns
|
|
394
|
+
* Uses Bun.Glob when available (faster), otherwise fast-glob
|
|
395
|
+
*/
|
|
396
|
+
async scanFiles() {
|
|
397
|
+
const patterns = this.config.include.map(
|
|
398
|
+
(p) => p.startsWith("/") || p.includes(":") ? p : `${this.config.rootDir}/${p}`
|
|
399
|
+
);
|
|
400
|
+
if (isBunRuntime && globalThis.Bun?.Glob) {
|
|
401
|
+
const files2 = [];
|
|
402
|
+
for (const pattern of patterns) {
|
|
403
|
+
const glob = new globalThis.Bun.Glob(pattern);
|
|
404
|
+
for await (const file of glob.scan({ onlyFiles: true, ignore: this.config.exclude })) {
|
|
405
|
+
files2.push(file);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
return files2;
|
|
409
|
+
}
|
|
410
|
+
const files = await fg(patterns, {
|
|
411
|
+
ignore: this.config.exclude,
|
|
412
|
+
onlyFiles: true,
|
|
413
|
+
absolute: true
|
|
414
|
+
});
|
|
415
|
+
return files;
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* Start Bun.watch() - fastest option
|
|
419
|
+
*/
|
|
420
|
+
async startBunWatcher(files) {
|
|
421
|
+
const dirs = /* @__PURE__ */ new Set();
|
|
422
|
+
for (const file of files) {
|
|
423
|
+
dirs.add(dirname(file));
|
|
424
|
+
this.watchedFiles.add(file);
|
|
425
|
+
}
|
|
426
|
+
this.bunWatcher = Bun.watch(this.config.rootDir, {
|
|
427
|
+
recursive: true,
|
|
428
|
+
filter: (path) => this.matchesPatterns(path)
|
|
429
|
+
});
|
|
430
|
+
this.bunWatcher.on("change", (event, path) => {
|
|
431
|
+
if (!path) return;
|
|
432
|
+
const fullPath = join(this.config.rootDir, path).replace(/\\/g, "/");
|
|
433
|
+
if (event === "rename") {
|
|
434
|
+
stat(fullPath).then(() => this.queueChange(fullPath, this.watchedFiles.has(fullPath) ? "change" : "add")).catch(() => this.queueChange(fullPath, "unlink"));
|
|
435
|
+
} else {
|
|
436
|
+
this.queueChange(fullPath, "change");
|
|
437
|
+
}
|
|
438
|
+
});
|
|
439
|
+
log.i("FILEWATCHER", "Bun.watch started", { dirs: dirs.size, files: files.length });
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* Start Node.js fs.watch()
|
|
443
|
+
*/
|
|
444
|
+
async startNodeWatcher(files) {
|
|
445
|
+
const dirs = /* @__PURE__ */ new Set();
|
|
446
|
+
for (const file of files) {
|
|
447
|
+
dirs.add(dirname(file));
|
|
448
|
+
this.watchedFiles.add(file);
|
|
449
|
+
}
|
|
450
|
+
for (const dir of dirs) {
|
|
451
|
+
try {
|
|
452
|
+
const watcher = watch(dir, { persistent: true }, (event, filename) => {
|
|
453
|
+
if (!filename) return;
|
|
454
|
+
const fullPath = join(dir, filename).replace(/\\/g, "/");
|
|
455
|
+
if (!this.matchesPatterns(fullPath)) return;
|
|
456
|
+
if (event === "rename") {
|
|
457
|
+
stat(fullPath).then(() => this.queueChange(fullPath, this.watchedFiles.has(fullPath) ? "change" : "add")).catch(() => this.queueChange(fullPath, "unlink"));
|
|
458
|
+
} else {
|
|
459
|
+
this.queueChange(fullPath, "change");
|
|
460
|
+
}
|
|
461
|
+
});
|
|
462
|
+
watcher.on("error", (err) => {
|
|
463
|
+
log.w("FILEWATCHER", "Watcher error", { dir, error: err.message });
|
|
464
|
+
});
|
|
465
|
+
this.watchers.set(dir, watcher);
|
|
466
|
+
} catch (err) {
|
|
467
|
+
log.w("FILEWATCHER", "Failed to watch directory", { dir, error: err.message });
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
log.i("FILEWATCHER", "fs.watch started", { dirs: this.watchers.size, files: files.length });
|
|
471
|
+
}
|
|
472
|
+
/**
|
|
473
|
+
* Check if path matches include/exclude patterns
|
|
474
|
+
*/
|
|
475
|
+
matchesPatterns(path) {
|
|
476
|
+
const normalizedPath = path.replace(/\\/g, "/");
|
|
477
|
+
for (const pattern of this.config.exclude) {
|
|
478
|
+
if (this.simpleMatch(normalizedPath, pattern)) {
|
|
479
|
+
return false;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
for (const pattern of this.config.include) {
|
|
483
|
+
if (this.simpleMatch(normalizedPath, pattern)) {
|
|
484
|
+
return true;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
return false;
|
|
488
|
+
}
|
|
489
|
+
/**
|
|
490
|
+
* Simple glob matching for common patterns
|
|
491
|
+
*/
|
|
492
|
+
simpleMatch(path, pattern) {
|
|
493
|
+
if (pattern.startsWith("**/")) {
|
|
494
|
+
const suffix = pattern.slice(3);
|
|
495
|
+
if (suffix.includes("*")) {
|
|
496
|
+
const ext = suffix.replace("*", "");
|
|
497
|
+
return path.endsWith(ext) || path.includes(`/${suffix.replace("*", "")}`);
|
|
498
|
+
}
|
|
499
|
+
return path.includes(`/${suffix}`) || path.endsWith(`/${suffix}`);
|
|
500
|
+
}
|
|
501
|
+
if (pattern.startsWith("*.")) {
|
|
502
|
+
return path.endsWith(pattern.slice(1));
|
|
503
|
+
}
|
|
504
|
+
if (pattern === "**/*") {
|
|
505
|
+
return true;
|
|
506
|
+
}
|
|
507
|
+
return path === pattern || path.endsWith(`/${pattern}`);
|
|
508
|
+
}
|
|
509
|
+
/**
|
|
510
|
+
* Queue a change for debounced emission
|
|
511
|
+
*/
|
|
512
|
+
queueChange(path, type) {
|
|
513
|
+
if (!this.isRunning) return;
|
|
514
|
+
if (type === "add") {
|
|
515
|
+
this.watchedFiles.add(path);
|
|
516
|
+
} else if (type === "unlink") {
|
|
517
|
+
this.watchedFiles.delete(path);
|
|
518
|
+
}
|
|
519
|
+
this.pendingChanges.set(path, {
|
|
520
|
+
path,
|
|
521
|
+
type,
|
|
522
|
+
timestamp: Date.now()
|
|
523
|
+
});
|
|
524
|
+
this.scheduleFlush();
|
|
525
|
+
}
|
|
526
|
+
/**
|
|
527
|
+
* Schedule debounced flush (Bun-compatible using async sleep)
|
|
528
|
+
*/
|
|
529
|
+
scheduleFlush() {
|
|
530
|
+
if (this.debounceAbort) {
|
|
531
|
+
this.debounceAbort.abort();
|
|
532
|
+
}
|
|
533
|
+
const abortController = new AbortController();
|
|
534
|
+
this.debounceAbort = abortController;
|
|
535
|
+
(async () => {
|
|
536
|
+
await sleep(this.config.debounceMs);
|
|
537
|
+
if (!abortController.signal.aborted) {
|
|
538
|
+
this.flush();
|
|
539
|
+
}
|
|
540
|
+
})();
|
|
541
|
+
}
|
|
542
|
+
/**
|
|
543
|
+
* Flush pending changes
|
|
544
|
+
*/
|
|
545
|
+
flush() {
|
|
546
|
+
if (this.debounceAbort) {
|
|
547
|
+
this.debounceAbort.abort();
|
|
548
|
+
this.debounceAbort = null;
|
|
549
|
+
}
|
|
550
|
+
if (this.pendingChanges.size === 0) return;
|
|
551
|
+
const events = Array.from(this.pendingChanges.values());
|
|
552
|
+
this.pendingChanges.clear();
|
|
553
|
+
const isBulk = events.length >= this.config.bulkThreshold;
|
|
554
|
+
log.d("FILEWATCHER", "Emitting changes", { count: events.length, bulkMode: isBulk });
|
|
555
|
+
this.emit("change", events, isBulk);
|
|
556
|
+
}
|
|
557
|
+
/**
|
|
558
|
+
* Get status
|
|
559
|
+
*/
|
|
560
|
+
getStatus() {
|
|
561
|
+
return {
|
|
562
|
+
running: this.isRunning,
|
|
563
|
+
runtime: hasBunWatch ? "bun" : "node",
|
|
564
|
+
isBunRuntime,
|
|
565
|
+
hasBunWatch,
|
|
566
|
+
watchedFiles: this.watchedFiles.size,
|
|
567
|
+
watchedDirs: this.watchers.size,
|
|
568
|
+
pendingChanges: this.pendingChanges.size
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
};
|
|
572
|
+
async function createFileWatcher(config) {
|
|
573
|
+
const watcher = new FileWatcher(config);
|
|
574
|
+
await watcher.start();
|
|
575
|
+
return watcher;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// src/core/git-watcher.ts
|
|
579
|
+
init_logging();
|
|
580
|
+
function isBunRuntime2() {
|
|
581
|
+
return typeof globalThis.Bun !== "undefined";
|
|
582
|
+
}
|
|
583
|
+
async function sleep2(ms) {
|
|
584
|
+
if (typeof globalThis.Bun !== "undefined" && typeof globalThis.Bun.sleep === "function") {
|
|
585
|
+
await globalThis.Bun.sleep(ms);
|
|
586
|
+
} else {
|
|
587
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
var GitWatcher = class {
|
|
591
|
+
config;
|
|
592
|
+
repoPath = null;
|
|
593
|
+
watcher = null;
|
|
594
|
+
commitPollRunning = false;
|
|
595
|
+
uncommittedPollRunning = false;
|
|
596
|
+
stopped = false;
|
|
597
|
+
// Flag to stop async loops
|
|
598
|
+
currentBranch = null;
|
|
599
|
+
currentCommit = null;
|
|
600
|
+
/** Tracks last known uncommitted files to detect changes */
|
|
601
|
+
lastUncommittedFiles = /* @__PURE__ */ new Set();
|
|
602
|
+
/** Debounce: accumulated pending changes */
|
|
603
|
+
pendingChanges = /* @__PURE__ */ new Set();
|
|
604
|
+
/** Debounce: abort controller for canceling pending debounce */
|
|
605
|
+
debounceAbortController = null;
|
|
606
|
+
/** Debounce delay in ms */
|
|
607
|
+
debounceMs;
|
|
608
|
+
/** Bulk mode threshold */
|
|
609
|
+
bulkModeThreshold;
|
|
610
|
+
branchChangeCallbacks = [];
|
|
611
|
+
commitCallbacks = [];
|
|
612
|
+
fileChangeCallbacks = [];
|
|
613
|
+
/** Callbacks specifically for uncommitted file changes */
|
|
614
|
+
uncommittedChangeCallbacks = [];
|
|
615
|
+
/** Debounced callbacks with bulk mode flag */
|
|
616
|
+
debouncedChangeCallbacks = [];
|
|
617
|
+
constructor(config) {
|
|
618
|
+
this.config = {
|
|
619
|
+
...config,
|
|
620
|
+
watchUncommitted: config.watchUncommitted ?? true,
|
|
621
|
+
uncommittedPollIntervalMs: config.uncommittedPollIntervalMs ?? 1e4,
|
|
622
|
+
includeUntracked: config.includeUntracked ?? true
|
|
623
|
+
};
|
|
624
|
+
this.debounceMs = config.debounceMs ?? 6e4;
|
|
625
|
+
this.bulkModeThreshold = config.bulkModeThreshold ?? 1e3;
|
|
626
|
+
}
|
|
627
|
+
/**
|
|
628
|
+
* Start watching a Git repository
|
|
629
|
+
*/
|
|
630
|
+
startWatching(repoPath) {
|
|
631
|
+
if (!this.config.enabled) {
|
|
632
|
+
log.i("GITWATCHER", "disabled");
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
this.repoPath = repoPath;
|
|
636
|
+
const gitHeadPath = join(repoPath, ".git", "HEAD");
|
|
637
|
+
if (!existsSync$1(gitHeadPath)) {
|
|
638
|
+
log.i("GITWATCHER", "no_git_dir");
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
this.currentBranch = this.getCurrentBranch();
|
|
642
|
+
this.currentCommit = this.getCurrentCommit();
|
|
643
|
+
log.i("GITWATCHER", "started", { path: repoPath });
|
|
644
|
+
log.i("GITWATCHER", "current_branch", { branch: this.currentBranch });
|
|
645
|
+
log.i("GITWATCHER", "current_commit", { commit: this.currentCommit });
|
|
646
|
+
this.stopped = false;
|
|
647
|
+
if (this.watcher) {
|
|
648
|
+
this.watcher.close();
|
|
649
|
+
this.watcher = null;
|
|
650
|
+
}
|
|
651
|
+
this.watcher = watch(gitHeadPath, (eventType) => {
|
|
652
|
+
if (eventType === "change") {
|
|
653
|
+
this.checkBranchChange();
|
|
654
|
+
}
|
|
655
|
+
});
|
|
656
|
+
this.startCommitPollLoop();
|
|
657
|
+
if (this.config.watchUncommitted) {
|
|
658
|
+
log.i("GITWATCHER", "uncommitted_watch_on", { interval: this.config.uncommittedPollIntervalMs });
|
|
659
|
+
this.getUncommittedFiles().then((files) => {
|
|
660
|
+
this.lastUncommittedFiles = new Set(files.map((f) => f.path));
|
|
661
|
+
log.i("GITWATCHER", "init_uncommitted", { count: this.lastUncommittedFiles.size });
|
|
662
|
+
});
|
|
663
|
+
this.startUncommittedPollLoop();
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
/** Timer handles for Node.js setInterval */
|
|
667
|
+
commitPollTimer;
|
|
668
|
+
uncommittedPollTimer;
|
|
669
|
+
/**
|
|
670
|
+
* Start commit and branch polling
|
|
671
|
+
* Node.js: uses setInterval
|
|
672
|
+
* Bun: uses async loop with Bun.sleep
|
|
673
|
+
* Also polls for branch changes as fs.watch() is unreliable on Windows
|
|
674
|
+
*/
|
|
675
|
+
startCommitPollLoop() {
|
|
676
|
+
if (this.commitPollRunning) return;
|
|
677
|
+
this.commitPollRunning = true;
|
|
678
|
+
if (isBunRuntime2()) {
|
|
679
|
+
(async () => {
|
|
680
|
+
while (!this.stopped) {
|
|
681
|
+
await sleep2(this.config.pollIntervalMs);
|
|
682
|
+
if (this.stopped) break;
|
|
683
|
+
try {
|
|
684
|
+
this.checkBranchChange();
|
|
685
|
+
this.checkCommitChange();
|
|
686
|
+
} catch (error) {
|
|
687
|
+
log.w("GITWATCHER", "poll_err", { err: String(error) });
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
this.commitPollRunning = false;
|
|
691
|
+
})();
|
|
692
|
+
} else {
|
|
693
|
+
this.commitPollTimer = setInterval(() => {
|
|
694
|
+
if (!this.stopped) {
|
|
695
|
+
try {
|
|
696
|
+
this.checkBranchChange();
|
|
697
|
+
this.checkCommitChange();
|
|
698
|
+
} catch (error) {
|
|
699
|
+
log.w("GITWATCHER", "poll_err", { err: String(error) });
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
}, this.config.pollIntervalMs);
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
/**
|
|
706
|
+
* Start uncommitted file polling
|
|
707
|
+
* Node.js: uses setInterval
|
|
708
|
+
* Bun: uses async loop with Bun.sleep
|
|
709
|
+
*/
|
|
710
|
+
startUncommittedPollLoop() {
|
|
711
|
+
if (this.uncommittedPollRunning) return;
|
|
712
|
+
this.uncommittedPollRunning = true;
|
|
713
|
+
if (isBunRuntime2()) {
|
|
714
|
+
(async () => {
|
|
715
|
+
while (!this.stopped) {
|
|
716
|
+
await sleep2(this.config.uncommittedPollIntervalMs);
|
|
717
|
+
if (this.stopped) break;
|
|
718
|
+
try {
|
|
719
|
+
await this.checkUncommittedChanges();
|
|
720
|
+
} catch (error) {
|
|
721
|
+
log.w("GITWATCHER", "uncommitted_poll_err", { err: String(error) });
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
this.uncommittedPollRunning = false;
|
|
725
|
+
})();
|
|
726
|
+
} else {
|
|
727
|
+
this.uncommittedPollTimer = setInterval(() => {
|
|
728
|
+
if (!this.stopped) {
|
|
729
|
+
this.checkUncommittedChanges().catch((error) => {
|
|
730
|
+
log.w("GITWATCHER", "uncommitted_poll_err", { err: String(error) });
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
}, this.config.uncommittedPollIntervalMs);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
/**
|
|
737
|
+
* Get the current tracked branch name
|
|
738
|
+
*/
|
|
739
|
+
getBranch() {
|
|
740
|
+
return this.currentBranch;
|
|
741
|
+
}
|
|
742
|
+
/**
|
|
743
|
+
* Check if watcher is currently active
|
|
744
|
+
*/
|
|
745
|
+
isWatching() {
|
|
746
|
+
return !this.stopped && (!!this.watcher || this.commitPollRunning || this.uncommittedPollRunning);
|
|
747
|
+
}
|
|
748
|
+
/**
|
|
749
|
+
* Stop watching the repository
|
|
750
|
+
*/
|
|
751
|
+
stopWatching() {
|
|
752
|
+
this.stopped = true;
|
|
753
|
+
if (this.commitPollTimer) {
|
|
754
|
+
clearInterval(this.commitPollTimer);
|
|
755
|
+
this.commitPollTimer = void 0;
|
|
756
|
+
}
|
|
757
|
+
if (this.uncommittedPollTimer) {
|
|
758
|
+
clearInterval(this.uncommittedPollTimer);
|
|
759
|
+
this.uncommittedPollTimer = void 0;
|
|
760
|
+
}
|
|
761
|
+
if (this.watcher) {
|
|
762
|
+
this.watcher.close();
|
|
763
|
+
this.watcher = null;
|
|
764
|
+
}
|
|
765
|
+
if (this.debounceAbortController) {
|
|
766
|
+
this.debounceAbortController.abort();
|
|
767
|
+
this.debounceAbortController = null;
|
|
768
|
+
}
|
|
769
|
+
this.lastUncommittedFiles.clear();
|
|
770
|
+
this.pendingChanges.clear();
|
|
771
|
+
log.i("GITWATCHER", "stopped");
|
|
772
|
+
}
|
|
773
|
+
/**
|
|
774
|
+
* Register callback for branch changes
|
|
775
|
+
*/
|
|
776
|
+
onBranchChange(callback) {
|
|
777
|
+
this.branchChangeCallbacks.push(callback);
|
|
778
|
+
}
|
|
779
|
+
/**
|
|
780
|
+
* Register callback for new commits
|
|
781
|
+
*/
|
|
782
|
+
onCommit(callback) {
|
|
783
|
+
this.commitCallbacks.push(callback);
|
|
784
|
+
}
|
|
785
|
+
/**
|
|
786
|
+
* Register callback for file changes (after commits)
|
|
787
|
+
*/
|
|
788
|
+
onFileChange(callback) {
|
|
789
|
+
this.fileChangeCallbacks.push(callback);
|
|
790
|
+
}
|
|
791
|
+
/**
|
|
792
|
+
* Register callback for uncommitted file changes (working directory)
|
|
793
|
+
* These are files that have been modified but not yet committed
|
|
794
|
+
*/
|
|
795
|
+
onUncommittedChange(callback) {
|
|
796
|
+
this.uncommittedChangeCallbacks.push(callback);
|
|
797
|
+
}
|
|
798
|
+
/**
|
|
799
|
+
* Register debounced callback for file changes.
|
|
800
|
+
* Callback receives accumulated files after debounce period and bulk mode flag.
|
|
801
|
+
* - bulkMode=true: many files changed, caller should drop/rebuild index
|
|
802
|
+
* - bulkMode=false: few files changed, caller should use incremental insert
|
|
803
|
+
*/
|
|
804
|
+
onDebouncedChange(callback) {
|
|
805
|
+
this.debouncedChangeCallbacks.push(callback);
|
|
806
|
+
}
|
|
807
|
+
/**
|
|
808
|
+
* Get count of pending changes waiting for debounce
|
|
809
|
+
*/
|
|
810
|
+
getPendingChangesCount() {
|
|
811
|
+
return this.pendingChanges.size;
|
|
812
|
+
}
|
|
813
|
+
/**
|
|
814
|
+
* Force flush pending changes immediately (bypasses debounce)
|
|
815
|
+
*/
|
|
816
|
+
forceFlush() {
|
|
817
|
+
if (this.debounceAbortController) {
|
|
818
|
+
this.debounceAbortController.abort();
|
|
819
|
+
this.debounceAbortController = null;
|
|
820
|
+
}
|
|
821
|
+
this.flushPendingChanges();
|
|
822
|
+
}
|
|
823
|
+
/**
|
|
824
|
+
* Flush accumulated pending changes and trigger debounced callbacks
|
|
825
|
+
*/
|
|
826
|
+
flushPendingChanges() {
|
|
827
|
+
if (this.pendingChanges.size === 0) return;
|
|
828
|
+
const files = Array.from(this.pendingChanges);
|
|
829
|
+
const bulkMode = files.length >= this.bulkModeThreshold;
|
|
830
|
+
const swaggerFiles = files.filter((f) => {
|
|
831
|
+
const lower = f.toLowerCase();
|
|
832
|
+
return lower.includes("swagger") || lower.includes("openapi") || lower.endsWith(".json") && (lower.includes("api") || lower.includes("spec"));
|
|
833
|
+
});
|
|
834
|
+
if (swaggerFiles.length > 0) {
|
|
835
|
+
log.i("GITWATCHER", "swagger_files_changed", {
|
|
836
|
+
swaggerFiles: swaggerFiles.length,
|
|
837
|
+
files: swaggerFiles,
|
|
838
|
+
warning: "Generated code may need regeneration"
|
|
839
|
+
});
|
|
840
|
+
}
|
|
841
|
+
log.i("GITWATCHER", "flushing", {
|
|
842
|
+
count: files.length,
|
|
843
|
+
bulkMode,
|
|
844
|
+
threshold: this.bulkModeThreshold,
|
|
845
|
+
swaggerChanges: swaggerFiles.length
|
|
846
|
+
});
|
|
847
|
+
this.pendingChanges.clear();
|
|
848
|
+
for (const callback of this.debouncedChangeCallbacks) {
|
|
849
|
+
try {
|
|
850
|
+
callback(files, bulkMode);
|
|
851
|
+
} catch (error) {
|
|
852
|
+
log.w("GITWATCHER", "debounce_cb_err", { err: String(error) });
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
/**
|
|
857
|
+
* Schedule debounced flush - resets timer on each call
|
|
858
|
+
*/
|
|
859
|
+
scheduleDebouncedFlush() {
|
|
860
|
+
if (this.debounceAbortController) {
|
|
861
|
+
this.debounceAbortController.abort();
|
|
862
|
+
}
|
|
863
|
+
const abortController = new AbortController();
|
|
864
|
+
this.debounceAbortController = abortController;
|
|
865
|
+
(async () => {
|
|
866
|
+
await sleep2(this.debounceMs);
|
|
867
|
+
if (!abortController.signal.aborted) {
|
|
868
|
+
this.debounceAbortController = null;
|
|
869
|
+
this.flushPendingChanges();
|
|
870
|
+
}
|
|
871
|
+
})();
|
|
872
|
+
log.i("GITWATCHER", "debounce_scheduled", {
|
|
873
|
+
pending: this.pendingChanges.size,
|
|
874
|
+
flushInSec: this.debounceMs / 1e3
|
|
875
|
+
});
|
|
876
|
+
}
|
|
877
|
+
/**
|
|
878
|
+
* Get files changed since a specific commit/branch
|
|
879
|
+
*/
|
|
880
|
+
async getChangedFiles(since) {
|
|
881
|
+
if (!this.repoPath) {
|
|
882
|
+
return [];
|
|
883
|
+
}
|
|
884
|
+
try {
|
|
885
|
+
const output = execSync(`git diff --name-status ${since}..HEAD`, {
|
|
886
|
+
cwd: this.repoPath,
|
|
887
|
+
encoding: "utf-8",
|
|
888
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
889
|
+
windowsHide: true
|
|
890
|
+
});
|
|
891
|
+
const changes = [];
|
|
892
|
+
const lines = output.trim().split("\n");
|
|
893
|
+
for (const line of lines) {
|
|
894
|
+
if (!line) continue;
|
|
895
|
+
const [status, ...pathParts] = line.split(" ");
|
|
896
|
+
if (!status) continue;
|
|
897
|
+
const path = pathParts.join(" ");
|
|
898
|
+
let changeStatus;
|
|
899
|
+
switch (status[0]) {
|
|
900
|
+
case "A":
|
|
901
|
+
changeStatus = "added";
|
|
902
|
+
break;
|
|
903
|
+
case "M":
|
|
904
|
+
changeStatus = "modified";
|
|
905
|
+
break;
|
|
906
|
+
case "D":
|
|
907
|
+
changeStatus = "deleted";
|
|
908
|
+
break;
|
|
909
|
+
case "R":
|
|
910
|
+
changeStatus = "renamed";
|
|
911
|
+
break;
|
|
912
|
+
default:
|
|
913
|
+
changeStatus = "modified";
|
|
914
|
+
}
|
|
915
|
+
changes.push({ path, status: changeStatus });
|
|
916
|
+
}
|
|
917
|
+
return changes;
|
|
918
|
+
} catch (error) {
|
|
919
|
+
log.w("GITWATCHER", "get_changed_fail", { err: String(error) });
|
|
920
|
+
return [];
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
/**
|
|
924
|
+
* Get changed files between two branches
|
|
925
|
+
*/
|
|
926
|
+
async getChangedFilesBetweenBranches(oldBranch, newBranch) {
|
|
927
|
+
if (!this.repoPath) {
|
|
928
|
+
return [];
|
|
929
|
+
}
|
|
930
|
+
try {
|
|
931
|
+
const output = execSync(`git diff --name-status ${oldBranch}...${newBranch}`, {
|
|
932
|
+
cwd: this.repoPath,
|
|
933
|
+
encoding: "utf-8",
|
|
934
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
935
|
+
windowsHide: true
|
|
936
|
+
});
|
|
937
|
+
const changes = [];
|
|
938
|
+
const lines = output.trim().split("\n");
|
|
939
|
+
for (const line of lines) {
|
|
940
|
+
if (!line) continue;
|
|
941
|
+
const [status, ...pathParts] = line.split(" ");
|
|
942
|
+
if (!status) continue;
|
|
943
|
+
const path = pathParts.join(" ");
|
|
944
|
+
let changeStatus;
|
|
945
|
+
switch (status[0]) {
|
|
946
|
+
case "A":
|
|
947
|
+
changeStatus = "added";
|
|
948
|
+
break;
|
|
949
|
+
case "M":
|
|
950
|
+
changeStatus = "modified";
|
|
951
|
+
break;
|
|
952
|
+
case "D":
|
|
953
|
+
changeStatus = "deleted";
|
|
954
|
+
break;
|
|
955
|
+
case "R":
|
|
956
|
+
changeStatus = "renamed";
|
|
957
|
+
break;
|
|
958
|
+
default:
|
|
959
|
+
changeStatus = "modified";
|
|
960
|
+
}
|
|
961
|
+
changes.push({ path, status: changeStatus });
|
|
962
|
+
}
|
|
963
|
+
return changes;
|
|
964
|
+
} catch (error) {
|
|
965
|
+
log.w("GITWATCHER", "get_branch_diff_fail", { err: String(error) });
|
|
966
|
+
return [];
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
// =============================================================================
|
|
970
|
+
// PRIVATE METHODS
|
|
971
|
+
// =============================================================================
|
|
972
|
+
getCurrentBranch() {
|
|
973
|
+
if (!this.repoPath) return null;
|
|
974
|
+
try {
|
|
975
|
+
const branch = execSync("git symbolic-ref --short HEAD", {
|
|
976
|
+
cwd: this.repoPath,
|
|
977
|
+
encoding: "utf-8",
|
|
978
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
979
|
+
windowsHide: true
|
|
980
|
+
}).trim();
|
|
981
|
+
return branch;
|
|
982
|
+
} catch {
|
|
983
|
+
try {
|
|
984
|
+
const hash = execSync("git rev-parse --short HEAD", {
|
|
985
|
+
cwd: this.repoPath,
|
|
986
|
+
encoding: "utf-8",
|
|
987
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
988
|
+
windowsHide: true
|
|
989
|
+
}).trim();
|
|
990
|
+
return `detached-${hash}`;
|
|
991
|
+
} catch {
|
|
992
|
+
return null;
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
getCurrentCommit() {
|
|
997
|
+
if (!this.repoPath) return null;
|
|
998
|
+
try {
|
|
999
|
+
const commit = execSync("git rev-parse HEAD", {
|
|
1000
|
+
cwd: this.repoPath,
|
|
1001
|
+
encoding: "utf-8",
|
|
1002
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
1003
|
+
windowsHide: true
|
|
1004
|
+
}).trim();
|
|
1005
|
+
return commit;
|
|
1006
|
+
} catch {
|
|
1007
|
+
return null;
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
checkBranchChange() {
|
|
1011
|
+
const newBranch = this.getCurrentBranch();
|
|
1012
|
+
if (newBranch && newBranch !== this.currentBranch) {
|
|
1013
|
+
const oldBranch = this.currentBranch || "unknown";
|
|
1014
|
+
log.i("GITWATCHER", "branch_changed", { from: oldBranch, to: newBranch });
|
|
1015
|
+
this.currentBranch = newBranch;
|
|
1016
|
+
this.currentCommit = this.getCurrentCommit();
|
|
1017
|
+
for (const callback of this.branchChangeCallbacks) {
|
|
1018
|
+
try {
|
|
1019
|
+
const result = callback(newBranch, oldBranch);
|
|
1020
|
+
if (result && typeof result.catch === "function") {
|
|
1021
|
+
result.catch((error) => {
|
|
1022
|
+
log.e("GITWATCHER", "branch_cb_async_err", { err: String(error) });
|
|
1023
|
+
});
|
|
1024
|
+
}
|
|
1025
|
+
} catch (error) {
|
|
1026
|
+
log.w("GITWATCHER", "branch_cb_err", { err: String(error) });
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
checkCommitChange() {
|
|
1032
|
+
const newCommit = this.getCurrentCommit();
|
|
1033
|
+
if (newCommit && newCommit !== this.currentCommit) {
|
|
1034
|
+
log.i("GITWATCHER", "new_commit", { commit: newCommit.slice(0, 8) });
|
|
1035
|
+
const oldCommit = this.currentCommit;
|
|
1036
|
+
this.currentCommit = newCommit;
|
|
1037
|
+
for (const callback of this.commitCallbacks) {
|
|
1038
|
+
try {
|
|
1039
|
+
callback(newCommit);
|
|
1040
|
+
} catch (error) {
|
|
1041
|
+
log.w("GITWATCHER", "commit_cb_err", { err: String(error) });
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
if (oldCommit) {
|
|
1045
|
+
this.getChangedFiles(oldCommit).then((files) => {
|
|
1046
|
+
if (files.length > 0) {
|
|
1047
|
+
for (const callback of this.fileChangeCallbacks) {
|
|
1048
|
+
try {
|
|
1049
|
+
callback(files.map((f) => f.path));
|
|
1050
|
+
} catch (error) {
|
|
1051
|
+
log.w("GITWATCHER", "file_cb_err", { err: String(error) });
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
});
|
|
1056
|
+
}
|
|
1057
|
+
this.lastUncommittedFiles.clear();
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
/**
|
|
1061
|
+
* Get uncommitted files (modified + staged + optionally untracked)
|
|
1062
|
+
* Uses `git status --porcelain` for efficient parsing
|
|
1063
|
+
*/
|
|
1064
|
+
async getUncommittedFiles() {
|
|
1065
|
+
if (!this.repoPath) {
|
|
1066
|
+
return [];
|
|
1067
|
+
}
|
|
1068
|
+
try {
|
|
1069
|
+
const untrackedFlag = this.config.includeUntracked ? "-uall" : "-uno";
|
|
1070
|
+
const output = execSync(`git status --porcelain ${untrackedFlag}`, {
|
|
1071
|
+
cwd: this.repoPath,
|
|
1072
|
+
encoding: "utf-8",
|
|
1073
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
1074
|
+
windowsHide: true
|
|
1075
|
+
});
|
|
1076
|
+
const changes = [];
|
|
1077
|
+
const lines = output.trim().split("\n");
|
|
1078
|
+
for (const line of lines) {
|
|
1079
|
+
if (!line || line.length < 3) continue;
|
|
1080
|
+
const indexStatus = line[0];
|
|
1081
|
+
const workTreeStatus = line[1];
|
|
1082
|
+
const filePath = line.slice(3).split(" -> ").pop() || line.slice(3);
|
|
1083
|
+
let changeStatus;
|
|
1084
|
+
const status = workTreeStatus !== " " ? workTreeStatus : indexStatus;
|
|
1085
|
+
switch (status) {
|
|
1086
|
+
case "A":
|
|
1087
|
+
case "?":
|
|
1088
|
+
changeStatus = "added";
|
|
1089
|
+
break;
|
|
1090
|
+
case "M":
|
|
1091
|
+
changeStatus = "modified";
|
|
1092
|
+
break;
|
|
1093
|
+
case "D":
|
|
1094
|
+
changeStatus = "deleted";
|
|
1095
|
+
break;
|
|
1096
|
+
case "R":
|
|
1097
|
+
changeStatus = "renamed";
|
|
1098
|
+
break;
|
|
1099
|
+
default:
|
|
1100
|
+
changeStatus = "modified";
|
|
1101
|
+
}
|
|
1102
|
+
changes.push({ path: filePath, status: changeStatus });
|
|
1103
|
+
}
|
|
1104
|
+
return changes;
|
|
1105
|
+
} catch (error) {
|
|
1106
|
+
log.w("GITWATCHER", "get_uncommitted_fail", { err: String(error) });
|
|
1107
|
+
return [];
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
/**
|
|
1111
|
+
* Check for uncommitted file changes and trigger callbacks
|
|
1112
|
+
*/
|
|
1113
|
+
async checkUncommittedChanges() {
|
|
1114
|
+
const currentFiles = await this.getUncommittedFiles();
|
|
1115
|
+
const currentSet = new Set(currentFiles.map((f) => f.path));
|
|
1116
|
+
const changedFiles = [];
|
|
1117
|
+
for (const file of currentFiles) {
|
|
1118
|
+
if (!this.lastUncommittedFiles.has(file.path)) {
|
|
1119
|
+
changedFiles.push(file.path);
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
for (const filePath of this.lastUncommittedFiles) {
|
|
1123
|
+
if (!currentSet.has(filePath)) {
|
|
1124
|
+
changedFiles.push(filePath);
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
this.lastUncommittedFiles = currentSet;
|
|
1128
|
+
if (changedFiles.length > 0) {
|
|
1129
|
+
log.i("GITWATCHER", "uncommitted_detected", { count: changedFiles.length });
|
|
1130
|
+
for (const callback of this.uncommittedChangeCallbacks) {
|
|
1131
|
+
try {
|
|
1132
|
+
callback(changedFiles);
|
|
1133
|
+
} catch (error) {
|
|
1134
|
+
log.w("GITWATCHER", "uncommitted_cb_err", { err: String(error) });
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
for (const callback of this.fileChangeCallbacks) {
|
|
1138
|
+
try {
|
|
1139
|
+
callback(changedFiles);
|
|
1140
|
+
} catch (error) {
|
|
1141
|
+
log.w("GITWATCHER", "file_cb_err", { err: String(error) });
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
if (this.debouncedChangeCallbacks.length > 0) {
|
|
1145
|
+
for (const file of changedFiles) {
|
|
1146
|
+
this.pendingChanges.add(file);
|
|
1147
|
+
}
|
|
1148
|
+
this.scheduleDebouncedFlush();
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
};
|
|
1153
|
+
|
|
1154
|
+
// src/agents/indexer-agent.ts
|
|
1155
|
+
init_logging();
|
|
1156
|
+
init_storage_paths();
|
|
1157
|
+
|
|
1158
|
+
// src/storage/batch-operations-libsql.ts
|
|
1159
|
+
init_logging();
|
|
1160
|
+
init_storage_paths();
|
|
1161
|
+
var DEFAULT_BATCH_SIZE = 1e3;
|
|
1162
|
+
var MAX_BATCH_SIZE = 2e3;
|
|
1163
|
+
var ID_LENGTH = 12;
|
|
1164
|
+
var yieldToEventLoop = () => new Promise((resolve) => setImmediate(resolve));
|
|
1165
|
+
var BatchOperationsLibSQL = class {
|
|
1166
|
+
batchSize;
|
|
1167
|
+
adapter;
|
|
1168
|
+
xxhashInstance = null;
|
|
1169
|
+
currentContext = {
|
|
1170
|
+
projectHash: "_unset_",
|
|
1171
|
+
branchName: "_unset_"
|
|
1172
|
+
};
|
|
1173
|
+
constructor(adapter, batchSize = DEFAULT_BATCH_SIZE) {
|
|
1174
|
+
this.adapter = adapter;
|
|
1175
|
+
this.batchSize = Math.min(batchSize, MAX_BATCH_SIZE);
|
|
1176
|
+
}
|
|
1177
|
+
setProjectContext(context) {
|
|
1178
|
+
log.i("BATCHOPS", "context_set", { ctx: `${context.projectHash}/${context.branchName}` });
|
|
1179
|
+
this.currentContext = context;
|
|
1180
|
+
this.adapter.setProjectContext(context);
|
|
1181
|
+
}
|
|
1182
|
+
setProject(projectPath, branchName) {
|
|
1183
|
+
const resolvedBranch = branchName ?? getCurrentGitBranchOrDefault(projectPath);
|
|
1184
|
+
this.setProjectContext({
|
|
1185
|
+
projectHash: getProjectHash(projectPath),
|
|
1186
|
+
branchName: resolvedBranch
|
|
1187
|
+
});
|
|
1188
|
+
}
|
|
1189
|
+
async initialize() {
|
|
1190
|
+
this.xxhashInstance = await xxhash();
|
|
1191
|
+
}
|
|
1192
|
+
destroy() {
|
|
1193
|
+
}
|
|
1194
|
+
// ---------------------------------------------------------------------------
|
|
1195
|
+
// Helpers: stable keys/ids
|
|
1196
|
+
// ---------------------------------------------------------------------------
|
|
1197
|
+
entityKey(e) {
|
|
1198
|
+
const isGlobal = e.type === "package" || e.type === "import";
|
|
1199
|
+
return isGlobal ? `${e.type}|${e.name}` : `${e.filePath}|${e.type}|${e.name}|${e.location?.start?.index ?? -1}-${e.location?.end?.index ?? -1}`;
|
|
1200
|
+
}
|
|
1201
|
+
stableEntityId(e) {
|
|
1202
|
+
if (!this.xxhashInstance) {
|
|
1203
|
+
throw new Error("BatchOperationsLibSQL not initialized - call initialize() first");
|
|
1204
|
+
}
|
|
1205
|
+
const key = this.entityKey(e);
|
|
1206
|
+
return this.xxhashInstance.h64ToString(key).slice(0, ID_LENGTH);
|
|
1207
|
+
}
|
|
1208
|
+
relationshipKey(r) {
|
|
1209
|
+
return `${r.fromId}|${r.toId}|${r.type}`;
|
|
1210
|
+
}
|
|
1211
|
+
stableRelationshipId(r) {
|
|
1212
|
+
if (!this.xxhashInstance) {
|
|
1213
|
+
throw new Error("BatchOperationsLibSQL not initialized - call initialize() first");
|
|
1214
|
+
}
|
|
1215
|
+
const key = this.relationshipKey(r);
|
|
1216
|
+
return this.xxhashInstance.h64ToString(key).slice(0, ID_LENGTH);
|
|
1217
|
+
}
|
|
1218
|
+
/**
|
|
1219
|
+
* Generate reverse relationships for bidirectional graph traversal.
|
|
1220
|
+
* For each CALLS relationship A→B, creates a CALLED_BY relationship B→A.
|
|
1221
|
+
* This enables trace_backwards to find callers efficiently.
|
|
1222
|
+
*/
|
|
1223
|
+
generateReverseRelationships(relationships) {
|
|
1224
|
+
const reverseMap = {
|
|
1225
|
+
["calls" /* CALLS */]: "called_by" /* CALLED_BY */,
|
|
1226
|
+
["imports" /* IMPORTS */]: "imported_by" /* IMPORTED_BY */,
|
|
1227
|
+
["references" /* REFERENCES */]: "referenced_by" /* REFERENCED_BY */,
|
|
1228
|
+
["extends" /* EXTENDS */]: "extended_by" /* EXTENDED_BY */,
|
|
1229
|
+
["implements" /* IMPLEMENTS */]: "implemented_by" /* IMPLEMENTED_BY */,
|
|
1230
|
+
// No reverse for these (already bidirectional or self-referential)
|
|
1231
|
+
["called_by" /* CALLED_BY */]: null,
|
|
1232
|
+
["imported_by" /* IMPORTED_BY */]: null,
|
|
1233
|
+
["referenced_by" /* REFERENCED_BY */]: null,
|
|
1234
|
+
["extended_by" /* EXTENDED_BY */]: null,
|
|
1235
|
+
["implemented_by" /* IMPLEMENTED_BY */]: null,
|
|
1236
|
+
["exports" /* EXPORTS */]: null,
|
|
1237
|
+
["contains" /* CONTAINS */]: null,
|
|
1238
|
+
["depends_on" /* DEPENDS_ON */]: null,
|
|
1239
|
+
["member_of" /* MEMBER_OF */]: null,
|
|
1240
|
+
["documents" /* DOCUMENTS */]: null,
|
|
1241
|
+
// NgRx relationships - no auto-reverse for now
|
|
1242
|
+
["dispatches_action" /* DISPATCHES_ACTION */]: null,
|
|
1243
|
+
["listens_to_action" /* LISTENS_TO_ACTION */]: null,
|
|
1244
|
+
["handles_action" /* HANDLES_ACTION */]: null,
|
|
1245
|
+
["selects_state" /* SELECTS_STATE */]: null,
|
|
1246
|
+
["modifies_state" /* MODIFIES_STATE */]: null,
|
|
1247
|
+
["produces_api" /* PRODUCES_API */]: null,
|
|
1248
|
+
["consumes_api" /* CONSUMES_API */]: null,
|
|
1249
|
+
["generated_from" /* GENERATED_FROM */]: null
|
|
1250
|
+
};
|
|
1251
|
+
const reverse = [];
|
|
1252
|
+
for (const r of relationships) {
|
|
1253
|
+
const reverseType = reverseMap[r.type];
|
|
1254
|
+
if (reverseType) {
|
|
1255
|
+
reverse.push({
|
|
1256
|
+
id: "",
|
|
1257
|
+
// Will be assigned stable ID later
|
|
1258
|
+
fromId: r.toId,
|
|
1259
|
+
toId: r.fromId,
|
|
1260
|
+
type: reverseType,
|
|
1261
|
+
metadata: { ...r.metadata, isReverse: true, originalType: r.type }
|
|
1262
|
+
});
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
return reverse;
|
|
1266
|
+
}
|
|
1267
|
+
// ---------------------------------------------------------------------------
|
|
1268
|
+
// Entity Operations
|
|
1269
|
+
// ---------------------------------------------------------------------------
|
|
1270
|
+
async insertEntities(entities, onProgress) {
|
|
1271
|
+
const start = Date.now();
|
|
1272
|
+
const errors = [];
|
|
1273
|
+
let totalProcessed = 0;
|
|
1274
|
+
const { projectHash, branchName } = this.currentContext;
|
|
1275
|
+
log.i("BATCHOPS", "insert_entities", { ctx: `${projectHash}/${branchName}`, count: entities.length });
|
|
1276
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1277
|
+
const uniq = [];
|
|
1278
|
+
for (const e of entities) {
|
|
1279
|
+
const key = this.entityKey(e);
|
|
1280
|
+
if (!seen.has(key)) {
|
|
1281
|
+
seen.add(key);
|
|
1282
|
+
uniq.push(e);
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
const batchCount = Math.ceil(uniq.length / this.batchSize);
|
|
1286
|
+
for (let i = 0; i < uniq.length; i += this.batchSize) {
|
|
1287
|
+
const batch = uniq.slice(i, Math.min(i + this.batchSize, uniq.length));
|
|
1288
|
+
const batchNum = Math.floor(i / this.batchSize);
|
|
1289
|
+
try {
|
|
1290
|
+
const entitiesWithIds = batch.map((entity) => {
|
|
1291
|
+
const now = Date.now();
|
|
1292
|
+
return {
|
|
1293
|
+
...entity,
|
|
1294
|
+
id: entity.id || this.stableEntityId(entity),
|
|
1295
|
+
createdAt: entity.createdAt || now,
|
|
1296
|
+
updatedAt: entity.updatedAt || now
|
|
1297
|
+
};
|
|
1298
|
+
});
|
|
1299
|
+
const result = await this.adapter.insertEntities(entitiesWithIds);
|
|
1300
|
+
totalProcessed += result.processed;
|
|
1301
|
+
if (result.errors.length > 0) {
|
|
1302
|
+
errors.push(...result.errors);
|
|
1303
|
+
}
|
|
1304
|
+
if (onProgress) {
|
|
1305
|
+
onProgress(totalProcessed, uniq.length);
|
|
1306
|
+
}
|
|
1307
|
+
if (batchNum < batchCount - 1) {
|
|
1308
|
+
await yieldToEventLoop();
|
|
1309
|
+
}
|
|
1310
|
+
} catch (error) {
|
|
1311
|
+
log.e("BATCHOPS", "batch_error", { err: String(error) });
|
|
1312
|
+
for (const entity of batch) {
|
|
1313
|
+
errors.push({
|
|
1314
|
+
item: entity,
|
|
1315
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1316
|
+
});
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
return {
|
|
1321
|
+
processed: totalProcessed,
|
|
1322
|
+
failed: errors.length,
|
|
1323
|
+
errors,
|
|
1324
|
+
timeMs: Date.now() - start
|
|
1325
|
+
};
|
|
1326
|
+
}
|
|
1327
|
+
async updateEntities(updates, onProgress) {
|
|
1328
|
+
const entities = updates.map((item) => {
|
|
1329
|
+
if ("changes" in item && item.id) {
|
|
1330
|
+
return {
|
|
1331
|
+
id: item.id,
|
|
1332
|
+
...item.changes,
|
|
1333
|
+
updatedAt: Date.now()
|
|
1334
|
+
};
|
|
1335
|
+
}
|
|
1336
|
+
return item;
|
|
1337
|
+
});
|
|
1338
|
+
return this.insertEntities(entities, onProgress);
|
|
1339
|
+
}
|
|
1340
|
+
async deleteEntities(entitiesOrIds, onProgress) {
|
|
1341
|
+
const start = Date.now();
|
|
1342
|
+
const errors = [];
|
|
1343
|
+
let totalProcessed = 0;
|
|
1344
|
+
const ids = entitiesOrIds.map((item) => {
|
|
1345
|
+
if (typeof item === "string") return item;
|
|
1346
|
+
return item.id || this.stableEntityId(item);
|
|
1347
|
+
});
|
|
1348
|
+
for (let i = 0; i < ids.length; i += this.batchSize) {
|
|
1349
|
+
const batch = ids.slice(i, Math.min(i + this.batchSize, ids.length));
|
|
1350
|
+
try {
|
|
1351
|
+
for (const id of batch) {
|
|
1352
|
+
await this.adapter.deleteEntity(id);
|
|
1353
|
+
totalProcessed++;
|
|
1354
|
+
}
|
|
1355
|
+
if (onProgress) {
|
|
1356
|
+
onProgress(totalProcessed, ids.length);
|
|
1357
|
+
}
|
|
1358
|
+
} catch (error) {
|
|
1359
|
+
for (const id of batch) {
|
|
1360
|
+
errors.push({
|
|
1361
|
+
item: id,
|
|
1362
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1363
|
+
});
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
return {
|
|
1368
|
+
processed: totalProcessed,
|
|
1369
|
+
failed: errors.length,
|
|
1370
|
+
errors,
|
|
1371
|
+
timeMs: Date.now() - start
|
|
1372
|
+
};
|
|
1373
|
+
}
|
|
1374
|
+
// ---------------------------------------------------------------------------
|
|
1375
|
+
// Relationship Operations
|
|
1376
|
+
// ---------------------------------------------------------------------------
|
|
1377
|
+
async insertRelationships(relationships, onProgress) {
|
|
1378
|
+
const start = Date.now();
|
|
1379
|
+
const errors = [];
|
|
1380
|
+
let totalProcessed = 0;
|
|
1381
|
+
const { projectHash, branchName } = this.currentContext;
|
|
1382
|
+
const reverseRels = this.generateReverseRelationships(relationships);
|
|
1383
|
+
const allRelationships = [...relationships, ...reverseRels];
|
|
1384
|
+
log.i("BATCHOPS", "insert_relationships", {
|
|
1385
|
+
ctx: `${projectHash}/${branchName}`,
|
|
1386
|
+
original: relationships.length,
|
|
1387
|
+
reverse: reverseRels.length,
|
|
1388
|
+
total: allRelationships.length
|
|
1389
|
+
});
|
|
1390
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1391
|
+
const uniq = [];
|
|
1392
|
+
for (const r of allRelationships) {
|
|
1393
|
+
const key = this.relationshipKey(r);
|
|
1394
|
+
if (!seen.has(key)) {
|
|
1395
|
+
seen.add(key);
|
|
1396
|
+
uniq.push(r);
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
const batchCount = Math.ceil(uniq.length / this.batchSize);
|
|
1400
|
+
for (let i = 0; i < uniq.length; i += this.batchSize) {
|
|
1401
|
+
const batch = uniq.slice(i, Math.min(i + this.batchSize, uniq.length));
|
|
1402
|
+
const batchNum = Math.floor(i / this.batchSize);
|
|
1403
|
+
try {
|
|
1404
|
+
const relsWithIds = batch.map((rel) => {
|
|
1405
|
+
const now = Date.now();
|
|
1406
|
+
return {
|
|
1407
|
+
...rel,
|
|
1408
|
+
id: this.stableRelationshipId(rel),
|
|
1409
|
+
createdAt: rel.createdAt || now
|
|
1410
|
+
};
|
|
1411
|
+
});
|
|
1412
|
+
const result = await this.adapter.insertRelationships(relsWithIds);
|
|
1413
|
+
totalProcessed += result.processed;
|
|
1414
|
+
if (result.errors.length > 0) {
|
|
1415
|
+
errors.push(...result.errors);
|
|
1416
|
+
}
|
|
1417
|
+
if (onProgress) {
|
|
1418
|
+
onProgress(totalProcessed, uniq.length);
|
|
1419
|
+
}
|
|
1420
|
+
if (batchNum < batchCount - 1) {
|
|
1421
|
+
await yieldToEventLoop();
|
|
1422
|
+
}
|
|
1423
|
+
} catch (error) {
|
|
1424
|
+
log.e("BATCHOPS", "rel_batch_error", { err: String(error) });
|
|
1425
|
+
for (const rel of batch) {
|
|
1426
|
+
errors.push({
|
|
1427
|
+
item: rel,
|
|
1428
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1429
|
+
});
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
return {
|
|
1434
|
+
processed: totalProcessed,
|
|
1435
|
+
failed: errors.length,
|
|
1436
|
+
errors,
|
|
1437
|
+
timeMs: Date.now() - start
|
|
1438
|
+
};
|
|
1439
|
+
}
|
|
1440
|
+
// ---------------------------------------------------------------------------
|
|
1441
|
+
// Utility Methods
|
|
1442
|
+
// ---------------------------------------------------------------------------
|
|
1443
|
+
optimizeBatchSize(avgProcessingTime) {
|
|
1444
|
+
if (avgProcessingTime < 50) {
|
|
1445
|
+
this.batchSize = Math.min(this.batchSize * 1.2, MAX_BATCH_SIZE);
|
|
1446
|
+
} else if (avgProcessingTime > 200) {
|
|
1447
|
+
this.batchSize = Math.max(this.batchSize * 0.8, 100);
|
|
1448
|
+
}
|
|
1449
|
+
this.batchSize = Math.floor(this.batchSize);
|
|
1450
|
+
}
|
|
1451
|
+
getBatchSize() {
|
|
1452
|
+
return this.batchSize;
|
|
1453
|
+
}
|
|
1454
|
+
};
|
|
1455
|
+
|
|
1456
|
+
// src/storage/cache-manager.ts
|
|
1457
|
+
init_logging();
|
|
1458
|
+
init_fast_hash();
|
|
1459
|
+
var DEFAULTS = {
|
|
1460
|
+
maxBytes: 100 * 1024 * 1024,
|
|
1461
|
+
maxEntries: 2e3,
|
|
1462
|
+
ttlMs: 5 * 60 * 1e3
|
|
1463
|
+
};
|
|
1464
|
+
function byteSize(v) {
|
|
1465
|
+
if (v == null) return 0;
|
|
1466
|
+
switch (typeof v) {
|
|
1467
|
+
case "string":
|
|
1468
|
+
return v.length * 2;
|
|
1469
|
+
case "number":
|
|
1470
|
+
return 8;
|
|
1471
|
+
case "boolean":
|
|
1472
|
+
return 4;
|
|
1473
|
+
}
|
|
1474
|
+
if (v instanceof Date) return 8;
|
|
1475
|
+
if (Array.isArray(v)) {
|
|
1476
|
+
let s = 24;
|
|
1477
|
+
for (let i = 0; i < v.length; i++) s += byteSize(v[i]);
|
|
1478
|
+
return s;
|
|
1479
|
+
}
|
|
1480
|
+
if (typeof v === "object") {
|
|
1481
|
+
let s = 24;
|
|
1482
|
+
const rec = v;
|
|
1483
|
+
for (const k of Object.keys(rec)) s += k.length * 2 + byteSize(rec[k]);
|
|
1484
|
+
return s;
|
|
1485
|
+
}
|
|
1486
|
+
return 24;
|
|
1487
|
+
}
|
|
1488
|
+
var QueryCacheManager = class {
|
|
1489
|
+
store;
|
|
1490
|
+
counters = { hits: 0, misses: 0, evictions: 0, sets: 0 };
|
|
1491
|
+
constructor(cfg = {}) {
|
|
1492
|
+
const ttl = cfg.defaultTTL ?? DEFAULTS.ttlMs;
|
|
1493
|
+
this.store = new LRUCache({
|
|
1494
|
+
max: cfg.maxEntries ?? DEFAULTS.maxEntries,
|
|
1495
|
+
maxSize: cfg.maxSize ?? DEFAULTS.maxBytes,
|
|
1496
|
+
sizeCalculation: (entry) => entry.size || byteSize(entry.value),
|
|
1497
|
+
dispose: (_v, key, reason) => {
|
|
1498
|
+
if (reason === "evict" || reason === "delete") {
|
|
1499
|
+
this.counters.evictions++;
|
|
1500
|
+
log.d("CACHEMGR", "evicted", { key, reason });
|
|
1501
|
+
}
|
|
1502
|
+
},
|
|
1503
|
+
ttl,
|
|
1504
|
+
updateAgeOnGet: true,
|
|
1505
|
+
updateAgeOnHas: false,
|
|
1506
|
+
allowStale: false
|
|
1507
|
+
});
|
|
1508
|
+
}
|
|
1509
|
+
get(key) {
|
|
1510
|
+
const entry = this.store.get(key);
|
|
1511
|
+
if (!entry) {
|
|
1512
|
+
this.counters.misses++;
|
|
1513
|
+
return null;
|
|
1514
|
+
}
|
|
1515
|
+
this.counters.hits++;
|
|
1516
|
+
entry.hits++;
|
|
1517
|
+
this.store.set(key, entry);
|
|
1518
|
+
return entry.value;
|
|
1519
|
+
}
|
|
1520
|
+
set(key, value, ttl) {
|
|
1521
|
+
const entry = {
|
|
1522
|
+
key,
|
|
1523
|
+
value,
|
|
1524
|
+
timestamp: Date.now(),
|
|
1525
|
+
ttl: ttl ?? DEFAULTS.ttlMs,
|
|
1526
|
+
hits: 0,
|
|
1527
|
+
size: byteSize(value)
|
|
1528
|
+
};
|
|
1529
|
+
this.store.set(key, entry);
|
|
1530
|
+
this.counters.sets++;
|
|
1531
|
+
}
|
|
1532
|
+
delete(key) {
|
|
1533
|
+
this.store.delete(key);
|
|
1534
|
+
}
|
|
1535
|
+
clear() {
|
|
1536
|
+
this.store.clear();
|
|
1537
|
+
log.i("CACHEMGR", "cache_cleared");
|
|
1538
|
+
}
|
|
1539
|
+
has(key) {
|
|
1540
|
+
return this.store.has(key);
|
|
1541
|
+
}
|
|
1542
|
+
prune() {
|
|
1543
|
+
const removed = this.store.purgeStale();
|
|
1544
|
+
if (removed) log.i("CACHEMGR", "pruned_stale");
|
|
1545
|
+
}
|
|
1546
|
+
getStats() {
|
|
1547
|
+
const total = this.counters.hits + this.counters.misses;
|
|
1548
|
+
return {
|
|
1549
|
+
size: this.store.size,
|
|
1550
|
+
hits: this.counters.hits,
|
|
1551
|
+
misses: this.counters.misses,
|
|
1552
|
+
hitRate: total > 0 ? this.counters.hits / total : 0,
|
|
1553
|
+
entries: this.store.size,
|
|
1554
|
+
evictions: this.counters.evictions,
|
|
1555
|
+
memoryUsage: this.store.calculatedSize || 0
|
|
1556
|
+
};
|
|
1557
|
+
}
|
|
1558
|
+
getEntries() {
|
|
1559
|
+
return Array.from(this.store.entries()).map(([key, value]) => ({ key, value }));
|
|
1560
|
+
}
|
|
1561
|
+
resetStats() {
|
|
1562
|
+
this.counters = { hits: 0, misses: 0, evictions: 0, sets: 0 };
|
|
1563
|
+
}
|
|
1564
|
+
static createKey(params) {
|
|
1565
|
+
const keys = Object.keys(params).sort();
|
|
1566
|
+
const ordered = {};
|
|
1567
|
+
for (const k of keys) ordered[k] = params[k];
|
|
1568
|
+
return hashText(JSON.stringify(ordered)).substring(0, 16);
|
|
1569
|
+
}
|
|
1570
|
+
};
|
|
1571
|
+
var instance = null;
|
|
1572
|
+
function getCacheManager(config) {
|
|
1573
|
+
if (!instance) instance = new QueryCacheManager(config);
|
|
1574
|
+
return instance;
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
// src/agents/indexer/entity-resolution.ts
|
|
1578
|
+
var CONTAINER_TYPES = /* @__PURE__ */ new Set(["class", "interface", "module", "namespace", "enum", "struct", "object"]);
|
|
1579
|
+
function buildEntityNameMap(entities) {
|
|
1580
|
+
const byName = /* @__PURE__ */ new Map();
|
|
1581
|
+
for (const e of entities) {
|
|
1582
|
+
const arr = byName.get(e.name) || [];
|
|
1583
|
+
arr.push(e);
|
|
1584
|
+
byName.set(e.name, arr);
|
|
1585
|
+
}
|
|
1586
|
+
return byName;
|
|
1587
|
+
}
|
|
1588
|
+
function addEntitiesToNameMap(byName, bySuffix, entities) {
|
|
1589
|
+
for (const e of entities) {
|
|
1590
|
+
const arr = byName.get(e.name) || [];
|
|
1591
|
+
arr.push(e);
|
|
1592
|
+
byName.set(e.name, arr);
|
|
1593
|
+
const dotIdx = e.name.lastIndexOf(".");
|
|
1594
|
+
if (dotIdx >= 0) {
|
|
1595
|
+
const suffix = e.name.slice(dotIdx);
|
|
1596
|
+
const suffArr = bySuffix.get(suffix) || [];
|
|
1597
|
+
suffArr.push(e);
|
|
1598
|
+
bySuffix.set(suffix, suffArr);
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
function resolveByNameAndLine(byName, name, line, sourceFile, preferContainerType, bySuffix) {
|
|
1603
|
+
if (name.startsWith("this.")) name = name.slice(5);
|
|
1604
|
+
let candidates = byName.get(name);
|
|
1605
|
+
if ((!candidates || candidates.length === 0) && !name.includes(".")) {
|
|
1606
|
+
const suffix = `.${name}`;
|
|
1607
|
+
if (bySuffix) {
|
|
1608
|
+
candidates = bySuffix.get(suffix);
|
|
1609
|
+
} else {
|
|
1610
|
+
candidates = [];
|
|
1611
|
+
for (const [entityName, entities] of byName) {
|
|
1612
|
+
if (entityName.endsWith(suffix)) {
|
|
1613
|
+
candidates.push(...entities);
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
}
|
|
1618
|
+
if (!candidates || candidates.length === 0) return void 0;
|
|
1619
|
+
if (sourceFile && candidates.length > 1) {
|
|
1620
|
+
const sameFile = candidates.filter((c) => c.filePath === sourceFile);
|
|
1621
|
+
if (sameFile.length > 0) {
|
|
1622
|
+
candidates = sameFile;
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
if (preferContainerType && candidates.length > 1) {
|
|
1626
|
+
const containers = candidates.filter((c) => CONTAINER_TYPES.has(c.type));
|
|
1627
|
+
if (containers.length > 0) {
|
|
1628
|
+
candidates = containers;
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
if (line == null) return candidates[0]?.id;
|
|
1632
|
+
let best;
|
|
1633
|
+
let bestDelta = Infinity;
|
|
1634
|
+
for (const c of candidates) {
|
|
1635
|
+
const d = Math.abs((c.location?.start?.line ?? 0) - line);
|
|
1636
|
+
if (d < bestDelta) {
|
|
1637
|
+
best = c;
|
|
1638
|
+
bestDelta = d;
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
return best?.id;
|
|
1642
|
+
}
|
|
1643
|
+
var ID_LENGTH2 = 12;
|
|
1644
|
+
var xxhashInstance = null;
|
|
1645
|
+
async function initXXHash() {
|
|
1646
|
+
if (!xxhashInstance) {
|
|
1647
|
+
xxhashInstance = await xxhash();
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1650
|
+
function getXXHash() {
|
|
1651
|
+
if (!xxhashInstance) {
|
|
1652
|
+
throw new Error("xxHash not initialized - call initXXHash() first");
|
|
1653
|
+
}
|
|
1654
|
+
return xxhashInstance;
|
|
1655
|
+
}
|
|
1656
|
+
function stableEntityId(base) {
|
|
1657
|
+
const isGlobal = base.type === "package" || base.type === "import";
|
|
1658
|
+
const key = isGlobal ? `${base.type}|${base.name}` : `${base.filePath}|${base.type}|${base.name}|${base.location?.start?.index ?? -1}-${base.location?.end?.index ?? -1}`;
|
|
1659
|
+
return getXXHash().h64ToString(key).slice(0, ID_LENGTH2);
|
|
1660
|
+
}
|
|
1661
|
+
function stableRelationshipId(fromId, toId, type) {
|
|
1662
|
+
return getXXHash().h64ToString(`${fromId}|${toId}|${type}`).slice(0, ID_LENGTH2);
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
// src/agents/indexer/external-placeholder.ts
|
|
1666
|
+
function parseExternalId(extId) {
|
|
1667
|
+
let source = "unknown";
|
|
1668
|
+
let symbol = "unknown";
|
|
1669
|
+
if (extId.startsWith("external:")) {
|
|
1670
|
+
const rest = extId.slice("external:".length);
|
|
1671
|
+
if (/^[A-Za-z]:[\\/]/.test(rest)) {
|
|
1672
|
+
const lastColonIdx = rest.lastIndexOf(":");
|
|
1673
|
+
if (lastColonIdx > 2) {
|
|
1674
|
+
source = rest.slice(0, lastColonIdx);
|
|
1675
|
+
symbol = rest.slice(lastColonIdx + 1) || "unknown";
|
|
1676
|
+
} else {
|
|
1677
|
+
source = rest;
|
|
1678
|
+
}
|
|
1679
|
+
} else {
|
|
1680
|
+
const colonIdx = rest.indexOf(":");
|
|
1681
|
+
if (colonIdx !== -1) {
|
|
1682
|
+
source = rest.slice(0, colonIdx);
|
|
1683
|
+
symbol = rest.slice(colonIdx + 1) || "unknown";
|
|
1684
|
+
} else {
|
|
1685
|
+
source = rest;
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
return { source, symbol };
|
|
1690
|
+
}
|
|
1691
|
+
function createExternalPlaceholder(extId, seenExternal, placeholders) {
|
|
1692
|
+
const { source, symbol } = parseExternalId(extId);
|
|
1693
|
+
const isFilePath = source.includes("/") || source.includes("\\") || /\.\w+$/.test(source);
|
|
1694
|
+
const qualifiedName = isFilePath || source === "unknown" ? symbol : `${source}.${symbol}`;
|
|
1695
|
+
const placeholderBase = {
|
|
1696
|
+
name: qualifiedName,
|
|
1697
|
+
// Qualified name for cross-module resolution
|
|
1698
|
+
type: "import" /* IMPORT */,
|
|
1699
|
+
filePath: `external://${source}`,
|
|
1700
|
+
location: {
|
|
1701
|
+
start: { line: 0, column: 0, index: 0 },
|
|
1702
|
+
end: { line: 0, column: 0, index: 0 }
|
|
1703
|
+
},
|
|
1704
|
+
metadata: { isExternal: true, source, symbol, qualifiedName },
|
|
1705
|
+
hash: `external:${source}:${symbol}`
|
|
1706
|
+
};
|
|
1707
|
+
const placeholderId = stableEntityId(placeholderBase);
|
|
1708
|
+
if (!seenExternal.has(extId)) {
|
|
1709
|
+
seenExternal.set(extId, placeholderId);
|
|
1710
|
+
placeholders.push({
|
|
1711
|
+
...placeholderBase,
|
|
1712
|
+
id: placeholderId,
|
|
1713
|
+
createdAt: Date.now(),
|
|
1714
|
+
updatedAt: Date.now()
|
|
1715
|
+
});
|
|
1716
|
+
}
|
|
1717
|
+
return placeholderId;
|
|
1718
|
+
}
|
|
1719
|
+
async function processExternalRelationships(relationships, stableRelationshipIdFn, lookupEntityByName) {
|
|
1720
|
+
const seenExternal = /* @__PURE__ */ new Map();
|
|
1721
|
+
const placeholders = [];
|
|
1722
|
+
for (const rel of relationships) {
|
|
1723
|
+
if (typeof rel.fromId === "string" && rel.fromId.startsWith("external:")) {
|
|
1724
|
+
rel.fromId = await resolveOrCreatePlaceholder(rel.fromId, seenExternal, placeholders);
|
|
1725
|
+
}
|
|
1726
|
+
if (typeof rel.toId === "string" && rel.toId.startsWith("external:")) {
|
|
1727
|
+
rel.toId = await resolveOrCreatePlaceholder(rel.toId, seenExternal, placeholders);
|
|
1728
|
+
}
|
|
1729
|
+
rel.id = stableRelationshipIdFn(rel.fromId, rel.toId, rel.type);
|
|
1730
|
+
}
|
|
1731
|
+
return placeholders;
|
|
1732
|
+
}
|
|
1733
|
+
async function resolveOrCreatePlaceholder(extId, seenExternal, placeholders, lookupEntityByName) {
|
|
1734
|
+
const cached = seenExternal.get(extId);
|
|
1735
|
+
if (cached) return cached;
|
|
1736
|
+
parseExternalId(extId);
|
|
1737
|
+
return createExternalPlaceholder(extId, seenExternal, placeholders);
|
|
1738
|
+
}
|
|
1739
|
+
init_logging();
|
|
1740
|
+
async function handleUncommittedChanges(files, ctx) {
|
|
1741
|
+
if (!ctx.currentRepositoryPath || files.length === 0) {
|
|
1742
|
+
return;
|
|
1743
|
+
}
|
|
1744
|
+
log.i("GITWATCHER", "uncommitted_changes", { cnt: files.length });
|
|
1745
|
+
const absolutePaths = files.map((f) => isAbsolute(f) ? f : join(ctx.currentRepositoryPath, f));
|
|
1746
|
+
for (const filePath of absolutePaths) {
|
|
1747
|
+
knowledgeBus.publish(
|
|
1748
|
+
"file:changed",
|
|
1749
|
+
{
|
|
1750
|
+
filePath,
|
|
1751
|
+
changeType: "modified",
|
|
1752
|
+
source: "git-watcher"
|
|
1753
|
+
},
|
|
1754
|
+
ctx.agentId
|
|
1755
|
+
);
|
|
1756
|
+
}
|
|
1757
|
+
knowledgeBus.publish(
|
|
1758
|
+
"indexer:files:changed",
|
|
1759
|
+
{
|
|
1760
|
+
files: absolutePaths,
|
|
1761
|
+
count: absolutePaths.length,
|
|
1762
|
+
repositoryPath: ctx.currentRepositoryPath,
|
|
1763
|
+
source: "git-watcher-uncommitted"
|
|
1764
|
+
},
|
|
1765
|
+
ctx.agentId
|
|
1766
|
+
);
|
|
1767
|
+
log.d("GITWATCHER", "published_changes", { cnt: absolutePaths.length });
|
|
1768
|
+
}
|
|
1769
|
+
async function handleDebouncedEmbeddingGeneration(files, bulkMode, ctx) {
|
|
1770
|
+
if (!ctx.currentRepositoryPath || files.length === 0) {
|
|
1771
|
+
return;
|
|
1772
|
+
}
|
|
1773
|
+
log.d("GITWATCHER", "debounced_embed", { cnt: files.length, bulkMode });
|
|
1774
|
+
knowledgeBus.publish(
|
|
1775
|
+
"indexer:embeddings:generate",
|
|
1776
|
+
{
|
|
1777
|
+
files,
|
|
1778
|
+
count: files.length,
|
|
1779
|
+
bulkMode,
|
|
1780
|
+
repositoryPath: ctx.currentRepositoryPath,
|
|
1781
|
+
source: "git-watcher-debounced"
|
|
1782
|
+
},
|
|
1783
|
+
ctx.agentId
|
|
1784
|
+
);
|
|
1785
|
+
log.d("GITWATCHER", "embed_event_pub", { cnt: files.length, bulkMode });
|
|
1786
|
+
}
|
|
1787
|
+
function getChangedFilesBetweenBranches(oldBranch, newBranch, repoPath) {
|
|
1788
|
+
try {
|
|
1789
|
+
const output = execSync(`git diff --name-only ${oldBranch}...${newBranch}`, {
|
|
1790
|
+
cwd: repoPath,
|
|
1791
|
+
encoding: "utf-8",
|
|
1792
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
1793
|
+
windowsHide: true
|
|
1794
|
+
});
|
|
1795
|
+
const files = output.trim().split("\n").filter((f) => f.length > 0).map((f) => join(repoPath, f));
|
|
1796
|
+
return files;
|
|
1797
|
+
} catch (error) {
|
|
1798
|
+
log.w("GITWATCHER", "get_branch_diff_fail", { err: String(error) });
|
|
1799
|
+
return [];
|
|
1800
|
+
}
|
|
1801
|
+
}
|
|
1802
|
+
async function handleBranchChange(newBranch, oldBranch, ctx) {
|
|
1803
|
+
log.i("GITWATCHER", "branch_changed", { oldBranch, newBranch });
|
|
1804
|
+
if (!ctx.branchManager || !ctx.currentRepositoryPath) {
|
|
1805
|
+
log.w("GITWATCHER", "branch_mgr_not_init");
|
|
1806
|
+
return;
|
|
1807
|
+
}
|
|
1808
|
+
try {
|
|
1809
|
+
await ctx.branchManager.switchBranch(newBranch, ctx.currentRepositoryPath);
|
|
1810
|
+
const changedFiles = getChangedFilesBetweenBranches(oldBranch, newBranch, ctx.currentRepositoryPath);
|
|
1811
|
+
log.i("GITWATCHER", "branch_files_diff", { cnt: changedFiles.length, oldBranch, newBranch });
|
|
1812
|
+
knowledgeBus.publish(
|
|
1813
|
+
"indexer:branch:changed",
|
|
1814
|
+
{
|
|
1815
|
+
oldBranch,
|
|
1816
|
+
newBranch,
|
|
1817
|
+
repositoryPath: ctx.currentRepositoryPath,
|
|
1818
|
+
changedFiles: changedFiles.length
|
|
1819
|
+
},
|
|
1820
|
+
ctx.agentId
|
|
1821
|
+
);
|
|
1822
|
+
if (changedFiles.length > 0) {
|
|
1823
|
+
knowledgeBus.publish(
|
|
1824
|
+
"indexer:files:changed",
|
|
1825
|
+
{
|
|
1826
|
+
files: changedFiles,
|
|
1827
|
+
count: changedFiles.length,
|
|
1828
|
+
repositoryPath: ctx.currentRepositoryPath,
|
|
1829
|
+
source: "git-watcher-branch-switch"
|
|
1830
|
+
},
|
|
1831
|
+
ctx.agentId
|
|
1832
|
+
);
|
|
1833
|
+
log.i("GITWATCHER", "branch_reindex_triggered", { files: changedFiles.length });
|
|
1834
|
+
}
|
|
1835
|
+
log.i("GITWATCHER", "branch_switched", { newBranch });
|
|
1836
|
+
} catch (error) {
|
|
1837
|
+
log.e("GITWATCHER", "branch_change_fail", { err: String(error) });
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1840
|
+
async function runtimeSleep(ms) {
|
|
1841
|
+
await sleep(ms);
|
|
1842
|
+
}
|
|
1843
|
+
function scheduleEmbeddingGeneration(ctx, onTrigger) {
|
|
1844
|
+
if (ctx.abortController) {
|
|
1845
|
+
ctx.abortController.abort();
|
|
1846
|
+
}
|
|
1847
|
+
const newAbortController = new AbortController();
|
|
1848
|
+
ctx.setAbortController(newAbortController);
|
|
1849
|
+
const signal = newAbortController.signal;
|
|
1850
|
+
log.d("INDEXER", "embed_scheduled", { ms: ctx.debouncePeriodMs });
|
|
1851
|
+
(async () => {
|
|
1852
|
+
try {
|
|
1853
|
+
const startTime = Date.now();
|
|
1854
|
+
while (!signal.aborted && Date.now() - startTime < ctx.debouncePeriodMs) {
|
|
1855
|
+
await runtimeSleep(1e3);
|
|
1856
|
+
}
|
|
1857
|
+
if (!signal.aborted) {
|
|
1858
|
+
await onTrigger();
|
|
1859
|
+
}
|
|
1860
|
+
} catch (error) {
|
|
1861
|
+
}
|
|
1862
|
+
})();
|
|
1863
|
+
}
|
|
1864
|
+
async function triggerEmbeddingGeneration(ctx) {
|
|
1865
|
+
if (ctx.pendingGeneration) {
|
|
1866
|
+
log.d("INDEXER", "embed_pending_skip");
|
|
1867
|
+
return;
|
|
1868
|
+
}
|
|
1869
|
+
ctx.setPendingGeneration(true);
|
|
1870
|
+
log.i("INDEXER", "embed_batch_trigger");
|
|
1871
|
+
knowledgeBus.publish(
|
|
1872
|
+
"indexer:incremental:complete",
|
|
1873
|
+
{
|
|
1874
|
+
timestamp: Date.now(),
|
|
1875
|
+
reason: "debounced_after_incremental_update"
|
|
1876
|
+
},
|
|
1877
|
+
ctx.agentId
|
|
1878
|
+
);
|
|
1879
|
+
await runtimeSleep(5e3);
|
|
1880
|
+
ctx.setPendingGeneration(false);
|
|
1881
|
+
}
|
|
1882
|
+
async function buildRelationships(parsedEntities, storageEntities) {
|
|
1883
|
+
const relationships = [];
|
|
1884
|
+
const entityMap = /* @__PURE__ */ new Map();
|
|
1885
|
+
for (const entity of storageEntities) {
|
|
1886
|
+
entityMap.set(`${entity.name}:${entity.location.start.line}`, entity.id);
|
|
1887
|
+
}
|
|
1888
|
+
const len = Math.min(parsedEntities.length, storageEntities.length);
|
|
1889
|
+
for (let i = 0; i < len; i++) {
|
|
1890
|
+
const parsed = parsedEntities[i];
|
|
1891
|
+
const entity = storageEntities[i];
|
|
1892
|
+
if (parsed.type === "import" && parsed.importData) {
|
|
1893
|
+
for (const specifier of parsed.importData.specifiers) {
|
|
1894
|
+
relationships.push({
|
|
1895
|
+
id: nanoid(12),
|
|
1896
|
+
fromId: entity.id,
|
|
1897
|
+
toId: `external:${parsed.importData.source}:${specifier.imported || specifier.local}`,
|
|
1898
|
+
type: "imports" /* IMPORTS */,
|
|
1899
|
+
metadata: {
|
|
1900
|
+
line: parsed.location.start.line,
|
|
1901
|
+
column: parsed.location.start.column,
|
|
1902
|
+
context: `Import from ${parsed.importData.source}`
|
|
1903
|
+
}
|
|
1904
|
+
});
|
|
1905
|
+
}
|
|
1906
|
+
}
|
|
1907
|
+
if (parsed.references) {
|
|
1908
|
+
for (const ref of parsed.references) {
|
|
1909
|
+
const refKey = Array.from(entityMap.keys()).find((key) => key.startsWith(`${ref}:`));
|
|
1910
|
+
if (refKey) {
|
|
1911
|
+
relationships.push({
|
|
1912
|
+
id: nanoid(12),
|
|
1913
|
+
fromId: entity.id,
|
|
1914
|
+
toId: entityMap.get(refKey),
|
|
1915
|
+
type: "references" /* REFERENCES */,
|
|
1916
|
+
metadata: {
|
|
1917
|
+
line: parsed.location.start.line,
|
|
1918
|
+
column: parsed.location.start.column
|
|
1919
|
+
}
|
|
1920
|
+
});
|
|
1921
|
+
}
|
|
1922
|
+
}
|
|
1923
|
+
}
|
|
1924
|
+
if (parsed.children) {
|
|
1925
|
+
for (const child of parsed.children) {
|
|
1926
|
+
const childKey = `${child.name}:${child.location.start.line}`;
|
|
1927
|
+
const childId = entityMap.get(childKey);
|
|
1928
|
+
if (childId) {
|
|
1929
|
+
relationships.push({
|
|
1930
|
+
id: nanoid(12),
|
|
1931
|
+
fromId: entity.id,
|
|
1932
|
+
toId: childId,
|
|
1933
|
+
type: "contains" /* CONTAINS */,
|
|
1934
|
+
metadata: {
|
|
1935
|
+
line: child.location.start.line,
|
|
1936
|
+
column: child.location.start.column
|
|
1937
|
+
}
|
|
1938
|
+
});
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1941
|
+
}
|
|
1942
|
+
if (parsed.inheritance) {
|
|
1943
|
+
if (parsed.inheritance.baseClasses) {
|
|
1944
|
+
for (const baseClass of parsed.inheritance.baseClasses) {
|
|
1945
|
+
const baseKey = Array.from(entityMap.keys()).find((key) => key.startsWith(`${baseClass}:`));
|
|
1946
|
+
const targetId = baseKey ? entityMap.get(baseKey) : `external:${baseClass}`;
|
|
1947
|
+
relationships.push({
|
|
1948
|
+
id: nanoid(12),
|
|
1949
|
+
fromId: entity.id,
|
|
1950
|
+
toId: targetId,
|
|
1951
|
+
type: "extends" /* EXTENDS */,
|
|
1952
|
+
metadata: {
|
|
1953
|
+
line: parsed.location.start.line,
|
|
1954
|
+
column: parsed.location.start.column,
|
|
1955
|
+
context: `${parsed.name} extends ${baseClass}`
|
|
1956
|
+
}
|
|
1957
|
+
});
|
|
1958
|
+
}
|
|
1959
|
+
}
|
|
1960
|
+
if (parsed.inheritance.interfaces) {
|
|
1961
|
+
for (const iface of parsed.inheritance.interfaces) {
|
|
1962
|
+
const ifaceKey = Array.from(entityMap.keys()).find((key) => key.startsWith(`${iface}:`));
|
|
1963
|
+
const targetId = ifaceKey ? entityMap.get(ifaceKey) : `external:${iface}`;
|
|
1964
|
+
relationships.push({
|
|
1965
|
+
id: nanoid(12),
|
|
1966
|
+
fromId: entity.id,
|
|
1967
|
+
toId: targetId,
|
|
1968
|
+
type: "implements" /* IMPLEMENTS */,
|
|
1969
|
+
metadata: {
|
|
1970
|
+
line: parsed.location.start.line,
|
|
1971
|
+
column: parsed.location.start.column,
|
|
1972
|
+
context: `${parsed.name} implements ${iface}`
|
|
1973
|
+
}
|
|
1974
|
+
});
|
|
1975
|
+
}
|
|
1976
|
+
}
|
|
1977
|
+
}
|
|
1978
|
+
if (parsed.relationships) {
|
|
1979
|
+
for (const rel of parsed.relationships) {
|
|
1980
|
+
const effectiveTarget = rel.target.startsWith("this.") ? rel.target.slice(5) : rel.target;
|
|
1981
|
+
const targetKey = Array.from(entityMap.keys()).find((key) => {
|
|
1982
|
+
const entityName = key.split(":")[0] ?? "";
|
|
1983
|
+
if (entityName.startsWith("this.")) return false;
|
|
1984
|
+
return key.startsWith(`${effectiveTarget}:`) || key.includes(`.${effectiveTarget}:`);
|
|
1985
|
+
});
|
|
1986
|
+
const targetId = targetKey ? entityMap.get(targetKey) : `external:${rel.target}`;
|
|
1987
|
+
let relType;
|
|
1988
|
+
switch (rel.type) {
|
|
1989
|
+
case "calls":
|
|
1990
|
+
relType = "calls" /* CALLS */;
|
|
1991
|
+
break;
|
|
1992
|
+
case "inherits":
|
|
1993
|
+
relType = "extends" /* EXTENDS */;
|
|
1994
|
+
break;
|
|
1995
|
+
case "implements":
|
|
1996
|
+
relType = "implements" /* IMPLEMENTS */;
|
|
1997
|
+
break;
|
|
1998
|
+
case "imports":
|
|
1999
|
+
relType = "imports" /* IMPORTS */;
|
|
2000
|
+
break;
|
|
2001
|
+
case "contains":
|
|
2002
|
+
relType = "contains" /* CONTAINS */;
|
|
2003
|
+
break;
|
|
2004
|
+
case "member_of":
|
|
2005
|
+
relType = "member_of" /* MEMBER_OF */;
|
|
2006
|
+
break;
|
|
2007
|
+
case "depends_on":
|
|
2008
|
+
relType = "depends_on" /* DEPENDS_ON */;
|
|
2009
|
+
break;
|
|
2010
|
+
case "produces_api":
|
|
2011
|
+
relType = "produces_api" /* PRODUCES_API */;
|
|
2012
|
+
break;
|
|
2013
|
+
case "consumes_api":
|
|
2014
|
+
relType = "consumes_api" /* CONSUMES_API */;
|
|
2015
|
+
break;
|
|
2016
|
+
case "generated_from":
|
|
2017
|
+
relType = "generated_from" /* GENERATED_FROM */;
|
|
2018
|
+
break;
|
|
2019
|
+
default:
|
|
2020
|
+
relType = "references" /* REFERENCES */;
|
|
2021
|
+
break;
|
|
2022
|
+
}
|
|
2023
|
+
relationships.push({
|
|
2024
|
+
id: nanoid(12),
|
|
2025
|
+
fromId: entity.id,
|
|
2026
|
+
toId: targetId,
|
|
2027
|
+
type: relType,
|
|
2028
|
+
metadata: {
|
|
2029
|
+
line: parsed.location.start.line,
|
|
2030
|
+
column: parsed.location.start.column,
|
|
2031
|
+
context: `${parsed.name} ${rel.type} ${rel.target}`,
|
|
2032
|
+
originalType: rel.type,
|
|
2033
|
+
...rel.metadata
|
|
2034
|
+
}
|
|
2035
|
+
});
|
|
2036
|
+
}
|
|
2037
|
+
}
|
|
2038
|
+
}
|
|
2039
|
+
return relationships;
|
|
2040
|
+
}
|
|
2041
|
+
|
|
2042
|
+
// src/agents/indexer-agent.ts
|
|
2043
|
+
function getIndexerConfig() {
|
|
2044
|
+
const config = getConfig();
|
|
2045
|
+
return {
|
|
2046
|
+
maxConcurrency: config.indexer?.maxConcurrency ?? 8,
|
|
2047
|
+
memoryLimit: config.indexer?.memoryLimit ?? 512,
|
|
2048
|
+
priority: config.indexer?.priority ?? 7,
|
|
2049
|
+
batchSize: config.indexer?.batchSize ?? 1e3,
|
|
2050
|
+
// OPTIMIZATION: Increased cache size from 50MB to 100MB for better performance
|
|
2051
|
+
cacheSize: config.indexer?.cacheSize ?? 100 * 1024 * 1024,
|
|
2052
|
+
cacheTTL: config.indexer?.cacheTTL ?? 5 * 60 * 1e3
|
|
2053
|
+
};
|
|
2054
|
+
}
|
|
2055
|
+
var IndexerAgent = class extends BaseAgent {
|
|
2056
|
+
graphStorage;
|
|
2057
|
+
batchOps;
|
|
2058
|
+
cacheManager;
|
|
2059
|
+
branchManager = null;
|
|
2060
|
+
/** Per-project GitWatchers — each project gets its own watcher */
|
|
2061
|
+
gitWatchers = /* @__PURE__ */ new Map();
|
|
2062
|
+
fileWatcher = null;
|
|
2063
|
+
// Debounced embedding generation
|
|
2064
|
+
pendingEmbeddingGeneration = false;
|
|
2065
|
+
embeddingDebounceAbort = null;
|
|
2066
|
+
EMBEDDING_DEBOUNCE_MS = 6e4;
|
|
2067
|
+
// 1 minute
|
|
2068
|
+
currentRepositoryPath = null;
|
|
2069
|
+
subscriptionIds = [];
|
|
2070
|
+
ready = false;
|
|
2071
|
+
lastFileWatcherError = null;
|
|
2072
|
+
indexingStats = {
|
|
2073
|
+
entitiesIndexed: 0,
|
|
2074
|
+
relationshipsCreated: 0,
|
|
2075
|
+
filesProcessed: 0,
|
|
2076
|
+
totalIndexTime: 0,
|
|
2077
|
+
lastIndexTime: 0
|
|
2078
|
+
};
|
|
2079
|
+
// Batch accumulator for streaming indexing optimization
|
|
2080
|
+
// Accumulates entities/relationships and flushes in batches to reduce DB operations
|
|
2081
|
+
BATCH_FLUSH_THRESHOLD = 270;
|
|
2082
|
+
// Flush every 270 files (was 200→50); 3 batches overlaps better with parsing
|
|
2083
|
+
pendingStorageEntities = [];
|
|
2084
|
+
pendingRelationships = [];
|
|
2085
|
+
pendingParsedEntities = [];
|
|
2086
|
+
pendingFilesCount = 0;
|
|
2087
|
+
batchFlushPromise = null;
|
|
2088
|
+
// Incremental entity name map — avoids O(n²) full rebuild on each file
|
|
2089
|
+
entityNameMap = /* @__PURE__ */ new Map();
|
|
2090
|
+
entitySuffixMap = /* @__PURE__ */ new Map();
|
|
2091
|
+
// Idle flush timer - flushes pending batch if no activity for 10 seconds
|
|
2092
|
+
idleFlushAbort = null;
|
|
2093
|
+
IDLE_FLUSH_MS = 1e4;
|
|
2094
|
+
constructor() {
|
|
2095
|
+
super("indexer" /* INDEXER */, getIndexerConfig());
|
|
2096
|
+
log.i("INDEXER", "created", { id: this.id });
|
|
2097
|
+
}
|
|
2098
|
+
/**
|
|
2099
|
+
* Initialize the indexer agent
|
|
2100
|
+
*/
|
|
2101
|
+
async onInitialize() {
|
|
2102
|
+
const startTime = Date.now();
|
|
2103
|
+
log.t("INDEXER", `[IndexerAgent] \u25B6 onInitialize() START`);
|
|
2104
|
+
log.i("INDEXER", "init_start");
|
|
2105
|
+
log.t("INDEXER", `[IndexerAgent] \u25B6 initXXHash`);
|
|
2106
|
+
await initXXHash();
|
|
2107
|
+
log.t("INDEXER", `[IndexerAgent] \u25C0 initXXHash (${Date.now() - startTime}ms)`);
|
|
2108
|
+
const appConfig = getConfig();
|
|
2109
|
+
{
|
|
2110
|
+
log.d("INDEXER", "branch_aware_init");
|
|
2111
|
+
const dataDir = appConfig.indexing.dataDir || getDataDir();
|
|
2112
|
+
this.branchManager = new BranchManager({
|
|
2113
|
+
enabled: true,
|
|
2114
|
+
dataDir,
|
|
2115
|
+
maxBranchesPerRepo: appConfig.indexing.maxBranchesPerRepo || 10,
|
|
2116
|
+
maxTotalBranches: appConfig.indexing.maxTotalBranches || 50,
|
|
2117
|
+
evictionStrategy: appConfig.indexing.evictionStrategy || "LRU"
|
|
2118
|
+
});
|
|
2119
|
+
await this.branchManager.initialize();
|
|
2120
|
+
if (appConfig.git?.enabled && appConfig.git.watchBranchChanges) {
|
|
2121
|
+
log.i("INDEXER", "git_watch_enabled");
|
|
2122
|
+
}
|
|
2123
|
+
}
|
|
2124
|
+
log.t("INDEXER", `[IndexerAgent] \u25B6 getGraphStorage`);
|
|
2125
|
+
const gsStart = Date.now();
|
|
2126
|
+
this.graphStorage = await getGraphStorage();
|
|
2127
|
+
log.t("INDEXER", `[IndexerAgent] \u25C0 getGraphStorage (${Date.now() - gsStart}ms)`);
|
|
2128
|
+
if ("initialize" in this.graphStorage && typeof this.graphStorage.initialize === "function") {
|
|
2129
|
+
log.t("INDEXER", `[IndexerAgent] \u25B6 graphStorage.initialize`);
|
|
2130
|
+
await this.graphStorage.initialize();
|
|
2131
|
+
log.t("INDEXER", `[IndexerAgent] \u25C0 graphStorage.initialize (${Date.now() - gsStart}ms)`);
|
|
2132
|
+
}
|
|
2133
|
+
const config = getIndexerConfig();
|
|
2134
|
+
log.t("INDEXER", `[IndexerAgent] \u25B6 BatchOperationsLibSQL.initialize`);
|
|
2135
|
+
const batchStart = Date.now();
|
|
2136
|
+
const adapter = getLibSQLAdapter();
|
|
2137
|
+
if (!adapter) {
|
|
2138
|
+
throw new Error(
|
|
2139
|
+
`[${this.id}] LibSQLAdapter is required but not available - ensure getGraphStorage() was called first`
|
|
2140
|
+
);
|
|
2141
|
+
}
|
|
2142
|
+
this.batchOps = new BatchOperationsLibSQL(adapter, config.batchSize);
|
|
2143
|
+
await this.batchOps.initialize();
|
|
2144
|
+
log.t("INDEXER", `[IndexerAgent] \u25C0 BatchOperationsLibSQL.initialize (${Date.now() - batchStart}ms)`);
|
|
2145
|
+
this.cacheManager = getCacheManager({
|
|
2146
|
+
maxSize: config.cacheSize,
|
|
2147
|
+
defaultTTL: config.cacheTTL
|
|
2148
|
+
});
|
|
2149
|
+
this.subscribeToParseEvents();
|
|
2150
|
+
this.ready = true;
|
|
2151
|
+
log.i("INDEXER", "init_done");
|
|
2152
|
+
}
|
|
2153
|
+
/**
|
|
2154
|
+
* Set the project context for GraphStorage and BatchOperations.
|
|
2155
|
+
* v3: Must be called before indexing to ensure correct project_hash.
|
|
2156
|
+
*/
|
|
2157
|
+
setProjectContext(projectPath, branchName) {
|
|
2158
|
+
log.i("INDEXER", "set_project_ctx", { path: projectPath });
|
|
2159
|
+
if (this.graphStorage && typeof this.graphStorage.setProject === "function") {
|
|
2160
|
+
this.graphStorage.setProject(projectPath, branchName);
|
|
2161
|
+
log.d("INDEXER", "gs_ctx_set", { path: projectPath, branch: branchName || "main" });
|
|
2162
|
+
} else {
|
|
2163
|
+
log.w("INDEXER", "gs_ctx_not_ready");
|
|
2164
|
+
}
|
|
2165
|
+
if (this.batchOps && typeof this.batchOps.setProject === "function") {
|
|
2166
|
+
this.batchOps.setProject(projectPath, branchName);
|
|
2167
|
+
log.d("INDEXER", "batch_ctx_set", { path: projectPath, branch: branchName || "main" });
|
|
2168
|
+
} else {
|
|
2169
|
+
log.w("INDEXER", "batch_ctx_not_ready");
|
|
2170
|
+
}
|
|
2171
|
+
this.currentRepositoryPath = projectPath;
|
|
2172
|
+
this.getOrCreateWatcher(projectPath);
|
|
2173
|
+
}
|
|
2174
|
+
/**
|
|
2175
|
+
* Subscribe to parser events via knowledge bus
|
|
2176
|
+
*
|
|
2177
|
+
* NOTE: This is DISABLED because DevAgent directly calls indexerAgent.process()/enqueue()
|
|
2178
|
+
* after parsing. Subscribing to events would cause DOUBLE processing of each file.
|
|
2179
|
+
* Only enable this if IndexerAgent is used standalone without DevAgent.
|
|
2180
|
+
*/
|
|
2181
|
+
subscribeToParseEvents() {
|
|
2182
|
+
log.d("INDEXER", "parse_subs_disabled");
|
|
2183
|
+
}
|
|
2184
|
+
// NOTE: handleParseComplete and handleParseBatchComplete are removed because
|
|
2185
|
+
// DevAgent calls indexerAgent.process()/enqueue() directly after parsing.
|
|
2186
|
+
// Keeping these methods would cause unused code warnings.
|
|
2187
|
+
/**
|
|
2188
|
+
* Check if agent can process the task
|
|
2189
|
+
*/
|
|
2190
|
+
canProcessTask(_task) {
|
|
2191
|
+
return true;
|
|
2192
|
+
}
|
|
2193
|
+
/**
|
|
2194
|
+
* Process indexer tasks
|
|
2195
|
+
*/
|
|
2196
|
+
async processTask(task) {
|
|
2197
|
+
const indexerTask = task;
|
|
2198
|
+
switch (indexerTask.type) {
|
|
2199
|
+
case "index:entities":
|
|
2200
|
+
return await this.indexEntities(
|
|
2201
|
+
indexerTask.payload.entities,
|
|
2202
|
+
indexerTask.payload.filePath,
|
|
2203
|
+
indexerTask.payload.relationships
|
|
2204
|
+
);
|
|
2205
|
+
case "index:incremental":
|
|
2206
|
+
return await this.incrementalUpdate(indexerTask.payload.changes);
|
|
2207
|
+
case "query:graph":
|
|
2208
|
+
return await this.queryGraph(indexerTask.payload.query);
|
|
2209
|
+
case "query:subgraph":
|
|
2210
|
+
return await this.querySubgraph(indexerTask.payload.entityId, indexerTask.payload.depth || 2);
|
|
2211
|
+
default:
|
|
2212
|
+
throw new Error(`Unknown task type: ${indexerTask.type}`);
|
|
2213
|
+
}
|
|
2214
|
+
}
|
|
2215
|
+
/**
|
|
2216
|
+
* Index entities and build relationships
|
|
2217
|
+
*/
|
|
2218
|
+
async indexEntities(entities, filePath, providedRelationships) {
|
|
2219
|
+
const startTime = Date.now();
|
|
2220
|
+
const flatEntities = flattenParsedEntities(entities);
|
|
2221
|
+
const childrenExtracted = flatEntities.length - entities.length;
|
|
2222
|
+
const withLang = flatEntities.filter((e) => e.language).length;
|
|
2223
|
+
const langSample = flatEntities.filter((e) => e.language).slice(0, 2);
|
|
2224
|
+
const noLangSample = flatEntities.filter((e) => !e.language).slice(0, 2);
|
|
2225
|
+
log.i("INDEXER", "lang_check", {
|
|
2226
|
+
total: flatEntities.length,
|
|
2227
|
+
withLang,
|
|
2228
|
+
langSample: langSample.map((e) => ({ n: e.name, l: e.language })),
|
|
2229
|
+
noLangSample: noLangSample.map((e) => ({ n: e.name, t: e.type }))
|
|
2230
|
+
});
|
|
2231
|
+
log.d("INDEXER", "indexing", {
|
|
2232
|
+
entities: entities.length,
|
|
2233
|
+
flat: flatEntities.length,
|
|
2234
|
+
children: childrenExtracted,
|
|
2235
|
+
file: filePath
|
|
2236
|
+
});
|
|
2237
|
+
if (childrenExtracted > 0) {
|
|
2238
|
+
const sampleChildren = flatEntities.slice(entities.length, entities.length + 3);
|
|
2239
|
+
log.t("INDEXER", "sample_children", { sample: sampleChildren.map((c) => c.name).join(",") });
|
|
2240
|
+
}
|
|
2241
|
+
const storageEntities = [];
|
|
2242
|
+
const validParsed = [];
|
|
2243
|
+
const preErrors = [];
|
|
2244
|
+
const fileHash = nanoid(8);
|
|
2245
|
+
for (const parsed of flatEntities) {
|
|
2246
|
+
try {
|
|
2247
|
+
const hasValidStructure = parsed && typeof parsed === "object" && "name" in parsed && typeof parsed.name === "string" && "type" in parsed && parsed.type && "location" in parsed && parsed.location;
|
|
2248
|
+
if (!hasValidStructure) {
|
|
2249
|
+
const reasons = [];
|
|
2250
|
+
if (!parsed) reasons.push("null/undefined");
|
|
2251
|
+
else if (typeof parsed !== "object") reasons.push("not object");
|
|
2252
|
+
else {
|
|
2253
|
+
if (!("name" in parsed) || typeof parsed.name !== "string") reasons.push("no name");
|
|
2254
|
+
if (!("type" in parsed) || !parsed.type) reasons.push("no type");
|
|
2255
|
+
if (!("location" in parsed) || !parsed.location) reasons.push("no location");
|
|
2256
|
+
}
|
|
2257
|
+
const entityName = parsed && typeof parsed === "object" && "name" in parsed ? parsed.name : void 0;
|
|
2258
|
+
log.t("INDEXER", "rejected_entity", { name: entityName, reasons: reasons.join(",") });
|
|
2259
|
+
throw new Error("Invalid entity");
|
|
2260
|
+
}
|
|
2261
|
+
const isImport = parsed?.type === "import" && parsed?.importData?.source;
|
|
2262
|
+
const hasName = typeof parsed.name === "string" && parsed.name.trim().length > 0;
|
|
2263
|
+
const normalizedParsed = !hasName && isImport ? { ...parsed, name: `import:${parsed.importData?.source}` } : parsed;
|
|
2264
|
+
const entityFilePath = normalizedParsed.filePath || filePath;
|
|
2265
|
+
const base = parsedEntityToEntity(normalizedParsed, entityFilePath, fileHash);
|
|
2266
|
+
const entity = {
|
|
2267
|
+
...base,
|
|
2268
|
+
id: stableEntityId(base),
|
|
2269
|
+
createdAt: Date.now(),
|
|
2270
|
+
updatedAt: Date.now()
|
|
2271
|
+
};
|
|
2272
|
+
storageEntities.push(entity);
|
|
2273
|
+
validParsed.push(normalizedParsed);
|
|
2274
|
+
} catch (e) {
|
|
2275
|
+
preErrors.push({ item: parsed, error: e.message });
|
|
2276
|
+
}
|
|
2277
|
+
}
|
|
2278
|
+
const entityResult = await this.batchOps.insertEntities(storageEntities, (processed, total) => {
|
|
2279
|
+
log.t("INDEXER", "entity_progress", { processed, total });
|
|
2280
|
+
});
|
|
2281
|
+
log.d("INDEXER", "entity_insert_done", {
|
|
2282
|
+
processed: entityResult.processed,
|
|
2283
|
+
failed: entityResult.failed,
|
|
2284
|
+
errors: entityResult.errors.length
|
|
2285
|
+
});
|
|
2286
|
+
if (entityResult.failed > 0) {
|
|
2287
|
+
log.w("INDEXER", "entity_errors", { errs: entityResult.errors.slice(0, 3).map((e) => e.error) });
|
|
2288
|
+
}
|
|
2289
|
+
if (validParsed.length) {
|
|
2290
|
+
const entitiesWithPath = validParsed.map((entity, i) => ({
|
|
2291
|
+
...entity,
|
|
2292
|
+
id: storageEntities[i]?.id || entity.id,
|
|
2293
|
+
filePath
|
|
2294
|
+
}));
|
|
2295
|
+
knowledgeBus.publish("semantic:new_entities", entitiesWithPath, this.id);
|
|
2296
|
+
log.d("INDEXER", `Published semantic:new_entities EARLY`, {
|
|
2297
|
+
count: entitiesWithPath.length,
|
|
2298
|
+
file: filePath
|
|
2299
|
+
});
|
|
2300
|
+
}
|
|
2301
|
+
let relationships = [];
|
|
2302
|
+
if (providedRelationships && providedRelationships.length > 0) {
|
|
2303
|
+
const byName = buildEntityNameMap(storageEntities);
|
|
2304
|
+
log.t("INDEXER", "entity_names_sample", { sample: Array.from(byName.keys()).slice(0, 10) });
|
|
2305
|
+
const first3 = providedRelationships.slice(0, 3);
|
|
2306
|
+
log.t("INDEXER", "raw_rels_sample", { sample: first3.map((r) => `${r.from}->${r.to}`) });
|
|
2307
|
+
const relLoopStart = Date.now();
|
|
2308
|
+
log.t("INDEXER", "process_rels", { cnt: providedRelationships.length });
|
|
2309
|
+
let callsTotal = 0;
|
|
2310
|
+
let callsFromResolved = 0;
|
|
2311
|
+
let callsToResolved = 0;
|
|
2312
|
+
const callsSample = [];
|
|
2313
|
+
for (const rel of providedRelationships) {
|
|
2314
|
+
const relSourceFile = rel.sourceFile || filePath;
|
|
2315
|
+
const isContains = rel.type === "contains";
|
|
2316
|
+
let fromId = resolveByNameAndLine(byName, rel.from, rel.metadata?.line, relSourceFile, isContains);
|
|
2317
|
+
let toId = resolveByNameAndLine(byName, rel.to, rel.metadata?.line, rel.targetFile || relSourceFile);
|
|
2318
|
+
if (rel.type === "calls") {
|
|
2319
|
+
callsTotal++;
|
|
2320
|
+
if (fromId) callsFromResolved++;
|
|
2321
|
+
if (toId) callsToResolved++;
|
|
2322
|
+
if (callsSample.length < 3) {
|
|
2323
|
+
callsSample.push({
|
|
2324
|
+
from: rel.from,
|
|
2325
|
+
to: rel.to,
|
|
2326
|
+
fromId: fromId || `EXT:${rel.from}`,
|
|
2327
|
+
toId: toId || `EXT:${rel.to}`
|
|
2328
|
+
});
|
|
2329
|
+
}
|
|
2330
|
+
}
|
|
2331
|
+
if (relationships.length === 0) {
|
|
2332
|
+
log.t("INDEXER", "first_rel_resolve", { from: rel.from, fromId, to: rel.to, toId });
|
|
2333
|
+
}
|
|
2334
|
+
if (!fromId) {
|
|
2335
|
+
const src = rel.sourceFile || filePath || "unknown";
|
|
2336
|
+
fromId = `external:${src}:${rel.from}`;
|
|
2337
|
+
}
|
|
2338
|
+
if (!toId) {
|
|
2339
|
+
const src = rel.targetFile || "unknown";
|
|
2340
|
+
toId = `external:${src}:${rel.to}`;
|
|
2341
|
+
}
|
|
2342
|
+
if (fromId && toId) {
|
|
2343
|
+
relationships.push({
|
|
2344
|
+
id: stableRelationshipId(fromId, toId, rel.type),
|
|
2345
|
+
fromId,
|
|
2346
|
+
toId,
|
|
2347
|
+
type: rel.type,
|
|
2348
|
+
metadata: { line: rel.metadata?.line, context: rel.type },
|
|
2349
|
+
createdAt: Date.now()
|
|
2350
|
+
});
|
|
2351
|
+
} else {
|
|
2352
|
+
log.t("INDEXER", "rel_skipped", { from: rel.from, to: rel.to, fromId, toId });
|
|
2353
|
+
}
|
|
2354
|
+
}
|
|
2355
|
+
if (callsTotal > 0) {
|
|
2356
|
+
log.i("INDEXER", "calls_resolution_stats", {
|
|
2357
|
+
total: callsTotal,
|
|
2358
|
+
fromResolved: callsFromResolved,
|
|
2359
|
+
toResolved: callsToResolved,
|
|
2360
|
+
fromPct: Math.round(callsFromResolved / callsTotal * 100),
|
|
2361
|
+
toPct: Math.round(callsToResolved / callsTotal * 100)
|
|
2362
|
+
});
|
|
2363
|
+
if (callsSample.length > 0) {
|
|
2364
|
+
log.i("INDEXER", "calls_sample", { sample: callsSample });
|
|
2365
|
+
}
|
|
2366
|
+
const entityNamesSample = Array.from(byName.keys()).slice(0, 5);
|
|
2367
|
+
log.i("INDEXER", "entity_names_in_map", { sample: entityNamesSample, total: byName.size });
|
|
2368
|
+
}
|
|
2369
|
+
const relLoopMs = Date.now() - relLoopStart;
|
|
2370
|
+
log.i("INDEXER", "RelationshipLoop", {
|
|
2371
|
+
count: providedRelationships.length,
|
|
2372
|
+
builtCount: relationships.length,
|
|
2373
|
+
ms: relLoopMs
|
|
2374
|
+
});
|
|
2375
|
+
log.d("INDEXER", "using_provided_rels", { cnt: relationships.length, ms: relLoopMs });
|
|
2376
|
+
} else {
|
|
2377
|
+
relationships = await this.buildRelationshipsInternal(validParsed, storageEntities);
|
|
2378
|
+
log.d("INDEXER", "built_auto_rels", { cnt: relationships.length });
|
|
2379
|
+
}
|
|
2380
|
+
const externalPlaceholders = await processExternalRelationships(
|
|
2381
|
+
relationships,
|
|
2382
|
+
stableRelationshipId
|
|
2383
|
+
// No lookupEntityByName - just create placeholders (fast path)
|
|
2384
|
+
);
|
|
2385
|
+
if (externalPlaceholders.length > 0) {
|
|
2386
|
+
await this.batchOps.insertEntities(externalPlaceholders);
|
|
2387
|
+
}
|
|
2388
|
+
let relResult = {
|
|
2389
|
+
processed: 0,
|
|
2390
|
+
failed: 0,
|
|
2391
|
+
errors: []
|
|
2392
|
+
};
|
|
2393
|
+
let insertRelMs = 0;
|
|
2394
|
+
if (relationships.length > 0) {
|
|
2395
|
+
log.t("INDEXER", "insert_rels_start", { cnt: relationships.length });
|
|
2396
|
+
const insertRelStart = Date.now();
|
|
2397
|
+
relResult = await this.batchOps.insertRelationships(relationships);
|
|
2398
|
+
insertRelMs = Date.now() - insertRelStart;
|
|
2399
|
+
log.i("INDEXER", "InsertRelationships", {
|
|
2400
|
+
count: relationships.length,
|
|
2401
|
+
processed: relResult.processed,
|
|
2402
|
+
ms: insertRelMs
|
|
2403
|
+
});
|
|
2404
|
+
}
|
|
2405
|
+
const fileInfo = {
|
|
2406
|
+
path: filePath,
|
|
2407
|
+
hash: fileHash,
|
|
2408
|
+
lastIndexed: Date.now(),
|
|
2409
|
+
entityCount: storageEntities.length
|
|
2410
|
+
};
|
|
2411
|
+
await this.graphStorage.updateFileInfo(fileInfo);
|
|
2412
|
+
this.cacheManager.clear();
|
|
2413
|
+
const indexTime = Date.now() - startTime;
|
|
2414
|
+
this.indexingStats.entitiesIndexed += entityResult.processed;
|
|
2415
|
+
this.indexingStats.relationshipsCreated += relResult.processed;
|
|
2416
|
+
this.indexingStats.filesProcessed++;
|
|
2417
|
+
this.indexingStats.totalIndexTime += indexTime;
|
|
2418
|
+
this.indexingStats.lastIndexTime = indexTime;
|
|
2419
|
+
log.t("INDEXER", "pub_index_complete");
|
|
2420
|
+
knowledgeBus.publish(
|
|
2421
|
+
"index:complete",
|
|
2422
|
+
{
|
|
2423
|
+
filePath,
|
|
2424
|
+
entities: entityResult.processed,
|
|
2425
|
+
relationships: relResult.processed,
|
|
2426
|
+
timeMs: indexTime
|
|
2427
|
+
},
|
|
2428
|
+
this.id
|
|
2429
|
+
);
|
|
2430
|
+
log.t("INDEXER", "index_complete_pubbed");
|
|
2431
|
+
log.i("INDEXER", "indexed_done", { entities: entityResult.processed, rels: relResult.processed, ms: indexTime });
|
|
2432
|
+
const failed = entityResult.failed + relResult.failed + preErrors.length;
|
|
2433
|
+
const errors = [...entityResult.errors, ...relResult.errors, ...preErrors];
|
|
2434
|
+
const { timeMs: _throwAway, ...restBase } = entityResult;
|
|
2435
|
+
return {
|
|
2436
|
+
...restBase,
|
|
2437
|
+
failed,
|
|
2438
|
+
errors,
|
|
2439
|
+
timeMs: indexTime,
|
|
2440
|
+
entitiesIndexed: entityResult.processed,
|
|
2441
|
+
relationshipsCreated: relResult.processed
|
|
2442
|
+
};
|
|
2443
|
+
}
|
|
2444
|
+
/**
|
|
2445
|
+
* Build relationships from parsed entities
|
|
2446
|
+
* Delegates to extracted relationship-builder module
|
|
2447
|
+
*/
|
|
2448
|
+
async buildRelationshipsInternal(parsedEntities, storageEntities) {
|
|
2449
|
+
return buildRelationships(parsedEntities, storageEntities);
|
|
2450
|
+
}
|
|
2451
|
+
// ===========================================================================
|
|
2452
|
+
// BATCH ACCUMULATOR METHODS - Optimized streaming indexing
|
|
2453
|
+
// ===========================================================================
|
|
2454
|
+
/**
|
|
2455
|
+
* Reset idle flush timer - called when new entities are queued.
|
|
2456
|
+
* After IDLE_FLUSH_MS of inactivity, pending batch will be flushed.
|
|
2457
|
+
*/
|
|
2458
|
+
resetIdleFlushTimer() {
|
|
2459
|
+
if (this.idleFlushAbort) {
|
|
2460
|
+
this.idleFlushAbort.abort();
|
|
2461
|
+
}
|
|
2462
|
+
if (this.pendingFilesCount === 0) {
|
|
2463
|
+
return;
|
|
2464
|
+
}
|
|
2465
|
+
const abortController = new AbortController();
|
|
2466
|
+
this.idleFlushAbort = abortController;
|
|
2467
|
+
(async () => {
|
|
2468
|
+
await sleep(this.IDLE_FLUSH_MS);
|
|
2469
|
+
if (!abortController.signal.aborted && this.pendingFilesCount > 0) {
|
|
2470
|
+
log.d("INDEXER", "idle_flush", {
|
|
2471
|
+
pending: this.pendingFilesCount,
|
|
2472
|
+
entities: this.pendingStorageEntities.length,
|
|
2473
|
+
relationships: this.pendingRelationships.length
|
|
2474
|
+
});
|
|
2475
|
+
logMemory("INDEXER", { pendingFiles: this.pendingFilesCount });
|
|
2476
|
+
try {
|
|
2477
|
+
await this.flushPendingBatch();
|
|
2478
|
+
if (tryGarbageCollect(true)) {
|
|
2479
|
+
log.d("INDEXER", "gc_after_idle_flush");
|
|
2480
|
+
}
|
|
2481
|
+
} catch (err) {
|
|
2482
|
+
log.w("INDEXER", "Idle flush failed", { error: err.message });
|
|
2483
|
+
}
|
|
2484
|
+
}
|
|
2485
|
+
})();
|
|
2486
|
+
}
|
|
2487
|
+
/**
|
|
2488
|
+
* Queue entities for batch indexing (streaming mode optimization)
|
|
2489
|
+
* Accumulates data and flushes in batches to reduce DB operations
|
|
2490
|
+
*/
|
|
2491
|
+
queueForIndexing(entities, filePath, providedRelationships) {
|
|
2492
|
+
if (!entities || entities.length === 0) return;
|
|
2493
|
+
this.resetIdleFlushTimer();
|
|
2494
|
+
if (filePath.endsWith(".py") || filePath.endsWith(".cs")) {
|
|
2495
|
+
log.i("INDEXER", "queue_rels_debug", {
|
|
2496
|
+
file: filePath.split(/[/\\]/).pop(),
|
|
2497
|
+
entities: entities.length,
|
|
2498
|
+
relationships: providedRelationships?.length ?? 0,
|
|
2499
|
+
relSample: providedRelationships?.slice(0, 3).map((r) => `${r.from}->${r.to}:${r.type}`)
|
|
2500
|
+
});
|
|
2501
|
+
}
|
|
2502
|
+
const flatEntities = flattenParsedEntities(entities);
|
|
2503
|
+
const fileHash = nanoid(8);
|
|
2504
|
+
const newEntities = [];
|
|
2505
|
+
for (const parsed of flatEntities) {
|
|
2506
|
+
try {
|
|
2507
|
+
if (!parsed?.name || !parsed?.type || !parsed?.location) continue;
|
|
2508
|
+
const entityFilePath = parsed.filePath || filePath;
|
|
2509
|
+
const base = parsedEntityToEntity(parsed, entityFilePath, fileHash);
|
|
2510
|
+
const entity = {
|
|
2511
|
+
...base,
|
|
2512
|
+
id: stableEntityId(base),
|
|
2513
|
+
createdAt: Date.now(),
|
|
2514
|
+
updatedAt: Date.now()
|
|
2515
|
+
};
|
|
2516
|
+
newEntities.push(entity);
|
|
2517
|
+
if (filePath.endsWith(".cs") && (parsed.type === "class" || parsed.type === "interface")) {
|
|
2518
|
+
log.w("INDEXER", "csharp_entity_id_debug", {
|
|
2519
|
+
name: entity.name,
|
|
2520
|
+
type: entity.type,
|
|
2521
|
+
id: entity.id,
|
|
2522
|
+
entityFilePath: entityFilePath?.split(/[/\\]/).slice(-2).join("/"),
|
|
2523
|
+
parsedFilePath: parsed.filePath?.split(/[/\\]/).slice(-2).join("/"),
|
|
2524
|
+
paramFilePath: filePath?.split(/[/\\]/).slice(-2).join("/"),
|
|
2525
|
+
pathMatch: entityFilePath === filePath,
|
|
2526
|
+
startIndex: base.location?.start?.index,
|
|
2527
|
+
endIndex: base.location?.end?.index
|
|
2528
|
+
});
|
|
2529
|
+
}
|
|
2530
|
+
} catch {
|
|
2531
|
+
}
|
|
2532
|
+
}
|
|
2533
|
+
this.pendingStorageEntities.push(...newEntities);
|
|
2534
|
+
addEntitiesToNameMap(this.entityNameMap, this.entitySuffixMap, newEntities);
|
|
2535
|
+
if (providedRelationships && providedRelationships.length > 0) {
|
|
2536
|
+
if (filePath.endsWith(".swift")) {
|
|
2537
|
+
const callsRels = providedRelationships.filter((r) => r.type === "calls" && r.metadata?.["crossModule"]);
|
|
2538
|
+
if (callsRels.length > 0) {
|
|
2539
|
+
log.w("XMOD", "swift_cross_module", {
|
|
2540
|
+
file: filePath.split(/[/\\]/).pop(),
|
|
2541
|
+
crossModuleCalls: callsRels.length,
|
|
2542
|
+
pendingEntities: this.pendingStorageEntities.length,
|
|
2543
|
+
byNameSize: this.entityNameMap.size,
|
|
2544
|
+
serverPosterKeys: Array.from(this.entityNameMap.keys()).filter((k) => k.includes("ServerPoster")),
|
|
2545
|
+
calls: callsRels.slice(0, 5).map((r) => ({
|
|
2546
|
+
from: r.from,
|
|
2547
|
+
to: r.to,
|
|
2548
|
+
inByName: this.entityNameMap.has(r.to)
|
|
2549
|
+
}))
|
|
2550
|
+
});
|
|
2551
|
+
}
|
|
2552
|
+
}
|
|
2553
|
+
let csResolved = 0;
|
|
2554
|
+
let csUnresolved = 0;
|
|
2555
|
+
for (const rel of providedRelationships) {
|
|
2556
|
+
const relSourceFile = rel.sourceFile || filePath;
|
|
2557
|
+
const isContains = rel.type === "contains";
|
|
2558
|
+
let fromId = resolveByNameAndLine(
|
|
2559
|
+
this.entityNameMap,
|
|
2560
|
+
rel.from,
|
|
2561
|
+
rel.metadata?.line,
|
|
2562
|
+
relSourceFile,
|
|
2563
|
+
isContains,
|
|
2564
|
+
this.entitySuffixMap
|
|
2565
|
+
);
|
|
2566
|
+
let toId = resolveByNameAndLine(
|
|
2567
|
+
this.entityNameMap,
|
|
2568
|
+
rel.to,
|
|
2569
|
+
rel.metadata?.line,
|
|
2570
|
+
rel.targetFile || relSourceFile,
|
|
2571
|
+
void 0,
|
|
2572
|
+
this.entitySuffixMap
|
|
2573
|
+
);
|
|
2574
|
+
if (rel.metadata?.["crossModule"]) {
|
|
2575
|
+
log.w("XMOD", "resolution", {
|
|
2576
|
+
to: rel.to,
|
|
2577
|
+
resolved: !!toId,
|
|
2578
|
+
toId: toId || "UNRESOLVED"
|
|
2579
|
+
});
|
|
2580
|
+
}
|
|
2581
|
+
if (!fromId) fromId = `external:${relSourceFile}:${rel.from}`;
|
|
2582
|
+
if (!toId) toId = `external:${rel.targetFile || "unknown"}:${rel.to}`;
|
|
2583
|
+
if (filePath.endsWith(".cs") && rel.type === "contains" && csResolved + csUnresolved < 5) {
|
|
2584
|
+
log.w("INDEXER", "csharp_rel_id_debug", {
|
|
2585
|
+
file: filePath.split(/[/\\]/).pop(),
|
|
2586
|
+
from: rel.from,
|
|
2587
|
+
to: rel.to,
|
|
2588
|
+
fromId: fromId.slice(0, 16),
|
|
2589
|
+
toId: toId.slice(0, 16),
|
|
2590
|
+
fromIsExternal: fromId.startsWith("external:"),
|
|
2591
|
+
toIsExternal: toId.startsWith("external:"),
|
|
2592
|
+
sourceFile: relSourceFile?.split(/[/\\]/).slice(-2).join("/")
|
|
2593
|
+
});
|
|
2594
|
+
}
|
|
2595
|
+
if (filePath.endsWith(".cs")) {
|
|
2596
|
+
const fromResolved = !fromId.startsWith("external:");
|
|
2597
|
+
const toResolved = !toId.startsWith("external:");
|
|
2598
|
+
if (fromResolved && toResolved) csResolved++;
|
|
2599
|
+
else csUnresolved++;
|
|
2600
|
+
}
|
|
2601
|
+
this.pendingRelationships.push({
|
|
2602
|
+
id: stableRelationshipId(fromId, toId, rel.type),
|
|
2603
|
+
fromId,
|
|
2604
|
+
toId,
|
|
2605
|
+
type: rel.type,
|
|
2606
|
+
metadata: { line: rel.metadata?.line, context: rel.type },
|
|
2607
|
+
createdAt: Date.now()
|
|
2608
|
+
});
|
|
2609
|
+
}
|
|
2610
|
+
if (filePath.endsWith(".cs") && (csResolved > 0 || csUnresolved > 0)) {
|
|
2611
|
+
log.i("INDEXER", "csharp_rel_resolution", {
|
|
2612
|
+
file: filePath.split(/[/\\]/).pop(),
|
|
2613
|
+
resolved: csResolved,
|
|
2614
|
+
unresolved: csUnresolved,
|
|
2615
|
+
pendingEntities: this.pendingStorageEntities.length,
|
|
2616
|
+
byNameSize: this.entityNameMap.size,
|
|
2617
|
+
pendingRels: this.pendingRelationships.length,
|
|
2618
|
+
// Show entity names relevant to this file
|
|
2619
|
+
fileEntityNames: this.pendingStorageEntities.filter((e) => e.filePath === filePath).slice(0, 10).map((e) => `${e.name}(${e.type})`)
|
|
2620
|
+
});
|
|
2621
|
+
}
|
|
2622
|
+
}
|
|
2623
|
+
this.pendingParsedEntities.push({ entities: flatEntities, filePath });
|
|
2624
|
+
this.pendingFilesCount++;
|
|
2625
|
+
if (this.pendingFilesCount >= this.BATCH_FLUSH_THRESHOLD) {
|
|
2626
|
+
this.flushPendingBatch().catch((err) => {
|
|
2627
|
+
log.w("INDEXER", "Auto-flush failed", { error: err.message });
|
|
2628
|
+
});
|
|
2629
|
+
}
|
|
2630
|
+
}
|
|
2631
|
+
/**
|
|
2632
|
+
* Flush all pending entities/relationships to DB in one batch
|
|
2633
|
+
* Returns stats about what was flushed
|
|
2634
|
+
*/
|
|
2635
|
+
async flushPendingBatch() {
|
|
2636
|
+
if (this.batchFlushPromise) {
|
|
2637
|
+
await this.batchFlushPromise;
|
|
2638
|
+
}
|
|
2639
|
+
const entitiesToFlush = this.pendingStorageEntities;
|
|
2640
|
+
const relationshipsToFlush = this.pendingRelationships;
|
|
2641
|
+
const parsedToFlush = this.pendingParsedEntities;
|
|
2642
|
+
const filesCount = this.pendingFilesCount;
|
|
2643
|
+
this.pendingStorageEntities = [];
|
|
2644
|
+
this.pendingRelationships = [];
|
|
2645
|
+
this.pendingParsedEntities = [];
|
|
2646
|
+
this.pendingFilesCount = 0;
|
|
2647
|
+
this.entityNameMap = /* @__PURE__ */ new Map();
|
|
2648
|
+
this.entitySuffixMap = /* @__PURE__ */ new Map();
|
|
2649
|
+
if (entitiesToFlush.length === 0) {
|
|
2650
|
+
return { entities: 0, relationships: 0, files: 0 };
|
|
2651
|
+
}
|
|
2652
|
+
const flushStart = Date.now();
|
|
2653
|
+
this.batchFlushPromise = (async () => {
|
|
2654
|
+
const entityResult = await this.batchOps.insertEntities(entitiesToFlush);
|
|
2655
|
+
const stableIdLookup = /* @__PURE__ */ new Map();
|
|
2656
|
+
for (const e of entitiesToFlush) {
|
|
2657
|
+
stableIdLookup.set(`${e.name}|${e.type}|${e.filePath}`, e.id);
|
|
2658
|
+
}
|
|
2659
|
+
for (const { entities, filePath } of parsedToFlush) {
|
|
2660
|
+
const entitiesWithPath = entities.map((e) => {
|
|
2661
|
+
const fp = e.filePath || filePath;
|
|
2662
|
+
const dbId = stableIdLookup.get(`${e.name}|${e.type}|${fp}`);
|
|
2663
|
+
return { ...e, id: dbId || e.id, filePath: fp };
|
|
2664
|
+
});
|
|
2665
|
+
knowledgeBus.publish("semantic:new_entities", entitiesWithPath, this.id);
|
|
2666
|
+
}
|
|
2667
|
+
const externalPlaceholders = await processExternalRelationships(
|
|
2668
|
+
relationshipsToFlush,
|
|
2669
|
+
stableRelationshipId
|
|
2670
|
+
// No lookupEntityByName - just create placeholders (fast path)
|
|
2671
|
+
);
|
|
2672
|
+
if (externalPlaceholders.length > 0) {
|
|
2673
|
+
await this.batchOps.insertEntities(externalPlaceholders);
|
|
2674
|
+
}
|
|
2675
|
+
let relResult = { processed: 0 };
|
|
2676
|
+
if (relationshipsToFlush.length > 0) {
|
|
2677
|
+
const realRels = relationshipsToFlush.filter(
|
|
2678
|
+
(r) => !r.fromId.startsWith("external:") && !r.toId.startsWith("external:")
|
|
2679
|
+
);
|
|
2680
|
+
const externalRels = relationshipsToFlush.length - realRels.length;
|
|
2681
|
+
log.i("INDEXER", "flush_rels", {
|
|
2682
|
+
count: relationshipsToFlush.length,
|
|
2683
|
+
real: realRels.length,
|
|
2684
|
+
external: externalRels,
|
|
2685
|
+
sample: realRels.slice(0, 3).map((r) => `${r.fromId}->${r.toId}:${r.type}`)
|
|
2686
|
+
});
|
|
2687
|
+
const csEntities = entitiesToFlush.filter((e) => e.language === "csharp" && e.type === "class");
|
|
2688
|
+
if (csEntities.length > 0) {
|
|
2689
|
+
const csEntityIds = new Set(csEntities.map((e) => e.id));
|
|
2690
|
+
const allEntityIds = new Set(entitiesToFlush.map((e) => e.id));
|
|
2691
|
+
let csFromCount = 0;
|
|
2692
|
+
let csToCount = 0;
|
|
2693
|
+
let anyRefCount = 0;
|
|
2694
|
+
for (const r of relationshipsToFlush) {
|
|
2695
|
+
if (csEntityIds.has(r.fromId)) csFromCount++;
|
|
2696
|
+
if (csEntityIds.has(r.toId)) csToCount++;
|
|
2697
|
+
if (allEntityIds.has(r.fromId) || allEntityIds.has(r.toId)) anyRefCount++;
|
|
2698
|
+
}
|
|
2699
|
+
const samples = csEntities.slice(0, 3).map((e) => {
|
|
2700
|
+
const rels = relationshipsToFlush.filter((r) => r.fromId === e.id || r.toId === e.id);
|
|
2701
|
+
return {
|
|
2702
|
+
name: e.name,
|
|
2703
|
+
id: e.id,
|
|
2704
|
+
filePath: e.filePath?.split(/[/\\]/).slice(-2).join("/"),
|
|
2705
|
+
relsCount: rels.length,
|
|
2706
|
+
relSample: rels.slice(0, 3).map((r) => `${r.fromId.slice(0, 8)}\u2192${r.toId.slice(0, 8)}:${r.type}`)
|
|
2707
|
+
};
|
|
2708
|
+
});
|
|
2709
|
+
log.w("INDEXER", "flush_cs_crosscheck", {
|
|
2710
|
+
csClasses: csEntities.length,
|
|
2711
|
+
csClassRelsFrom: csFromCount,
|
|
2712
|
+
csClassRelsTo: csToCount,
|
|
2713
|
+
totalRels: relationshipsToFlush.length,
|
|
2714
|
+
relsRefAnyEntity: anyRefCount,
|
|
2715
|
+
samples
|
|
2716
|
+
});
|
|
2717
|
+
}
|
|
2718
|
+
relResult = await this.batchOps.insertRelationships(relationshipsToFlush);
|
|
2719
|
+
}
|
|
2720
|
+
const fileEntityCounts = /* @__PURE__ */ new Map();
|
|
2721
|
+
for (const { filePath, entities } of parsedToFlush) {
|
|
2722
|
+
const current = fileEntityCounts.get(filePath) || 0;
|
|
2723
|
+
fileEntityCounts.set(filePath, current + entities.length);
|
|
2724
|
+
}
|
|
2725
|
+
const fileInfoBatch = [];
|
|
2726
|
+
const now = Date.now();
|
|
2727
|
+
for (const [filePath, entityCount] of fileEntityCounts) {
|
|
2728
|
+
fileInfoBatch.push({
|
|
2729
|
+
path: filePath,
|
|
2730
|
+
hash: nanoid(8),
|
|
2731
|
+
lastIndexed: now,
|
|
2732
|
+
entityCount
|
|
2733
|
+
});
|
|
2734
|
+
}
|
|
2735
|
+
if (fileInfoBatch.length > 0) {
|
|
2736
|
+
if ("batchUpdateFileInfo" in this.graphStorage && typeof this.graphStorage.batchUpdateFileInfo === "function") {
|
|
2737
|
+
await this.graphStorage.batchUpdateFileInfo(fileInfoBatch);
|
|
2738
|
+
} else {
|
|
2739
|
+
for (const fi of fileInfoBatch) {
|
|
2740
|
+
await this.graphStorage.updateFileInfo(fi);
|
|
2741
|
+
}
|
|
2742
|
+
}
|
|
2743
|
+
}
|
|
2744
|
+
this.indexingStats.entitiesIndexed += entityResult.processed;
|
|
2745
|
+
this.indexingStats.relationshipsCreated += relResult.processed;
|
|
2746
|
+
this.indexingStats.filesProcessed += filesCount;
|
|
2747
|
+
log.i("INDEXER", "Batch flush completed", {
|
|
2748
|
+
entities: entityResult.processed,
|
|
2749
|
+
relationships: relResult.processed,
|
|
2750
|
+
files: filesCount,
|
|
2751
|
+
filesTracked: fileEntityCounts.size,
|
|
2752
|
+
ms: Date.now() - flushStart
|
|
2753
|
+
});
|
|
2754
|
+
})();
|
|
2755
|
+
await this.batchFlushPromise;
|
|
2756
|
+
this.batchFlushPromise = null;
|
|
2757
|
+
return {
|
|
2758
|
+
entities: entitiesToFlush.length,
|
|
2759
|
+
relationships: relationshipsToFlush.length,
|
|
2760
|
+
files: filesCount
|
|
2761
|
+
};
|
|
2762
|
+
}
|
|
2763
|
+
/**
|
|
2764
|
+
* Get pending batch stats (for monitoring)
|
|
2765
|
+
*/
|
|
2766
|
+
getPendingBatchStats() {
|
|
2767
|
+
return {
|
|
2768
|
+
entities: this.pendingStorageEntities.length,
|
|
2769
|
+
relationships: this.pendingRelationships.length,
|
|
2770
|
+
files: this.pendingFilesCount
|
|
2771
|
+
};
|
|
2772
|
+
}
|
|
2773
|
+
/**
|
|
2774
|
+
* Perform incremental update for changed entities
|
|
2775
|
+
*/
|
|
2776
|
+
async incrementalUpdate(changes) {
|
|
2777
|
+
log.d("INDEXER", "incr_update", { cnt: changes.length });
|
|
2778
|
+
const toAdd = [];
|
|
2779
|
+
const toUpdate = [];
|
|
2780
|
+
const toDelete = [];
|
|
2781
|
+
for (const change of changes) {
|
|
2782
|
+
switch (change.type) {
|
|
2783
|
+
case "added":
|
|
2784
|
+
if (change.entity) {
|
|
2785
|
+
toAdd.push(change.entity);
|
|
2786
|
+
}
|
|
2787
|
+
break;
|
|
2788
|
+
case "modified":
|
|
2789
|
+
if (change.entity && change.entityId) {
|
|
2790
|
+
toUpdate.push({
|
|
2791
|
+
id: change.entityId,
|
|
2792
|
+
changes: change.entity
|
|
2793
|
+
});
|
|
2794
|
+
}
|
|
2795
|
+
break;
|
|
2796
|
+
case "deleted":
|
|
2797
|
+
if (change.entityId) {
|
|
2798
|
+
toDelete.push(change.entityId);
|
|
2799
|
+
}
|
|
2800
|
+
break;
|
|
2801
|
+
}
|
|
2802
|
+
}
|
|
2803
|
+
let processed = 0;
|
|
2804
|
+
let failed = 0;
|
|
2805
|
+
const errors = [];
|
|
2806
|
+
if (toAdd.length > 0) {
|
|
2807
|
+
const result = await this.batchOps.insertEntities(toAdd);
|
|
2808
|
+
processed += result.processed;
|
|
2809
|
+
failed += result.failed;
|
|
2810
|
+
errors.push(...result.errors);
|
|
2811
|
+
}
|
|
2812
|
+
if (toUpdate.length > 0) {
|
|
2813
|
+
const result = await this.batchOps.updateEntities(toUpdate);
|
|
2814
|
+
processed += result.processed;
|
|
2815
|
+
failed += result.failed;
|
|
2816
|
+
errors.push(...result.errors);
|
|
2817
|
+
}
|
|
2818
|
+
if (toDelete.length > 0) {
|
|
2819
|
+
const result = await this.batchOps.deleteEntities(toDelete);
|
|
2820
|
+
processed += result.processed;
|
|
2821
|
+
failed += result.failed;
|
|
2822
|
+
errors.push(...result.errors);
|
|
2823
|
+
}
|
|
2824
|
+
this.cacheManager.clear();
|
|
2825
|
+
this.doScheduleEmbeddingGeneration();
|
|
2826
|
+
return {
|
|
2827
|
+
processed,
|
|
2828
|
+
failed,
|
|
2829
|
+
errors,
|
|
2830
|
+
timeMs: 0
|
|
2831
|
+
};
|
|
2832
|
+
}
|
|
2833
|
+
/**
|
|
2834
|
+
* Get embedding scheduler context for extracted functions
|
|
2835
|
+
*/
|
|
2836
|
+
getEmbeddingSchedulerContext() {
|
|
2837
|
+
return {
|
|
2838
|
+
agentId: this.id,
|
|
2839
|
+
debouncePeriodMs: this.EMBEDDING_DEBOUNCE_MS,
|
|
2840
|
+
abortController: this.embeddingDebounceAbort,
|
|
2841
|
+
pendingGeneration: this.pendingEmbeddingGeneration,
|
|
2842
|
+
setPendingGeneration: (value) => {
|
|
2843
|
+
this.pendingEmbeddingGeneration = value;
|
|
2844
|
+
},
|
|
2845
|
+
setAbortController: (controller) => {
|
|
2846
|
+
this.embeddingDebounceAbort = controller;
|
|
2847
|
+
}
|
|
2848
|
+
};
|
|
2849
|
+
}
|
|
2850
|
+
/**
|
|
2851
|
+
* Schedule debounced embedding generation
|
|
2852
|
+
* Waits 1 minute after last change before triggering generation
|
|
2853
|
+
*/
|
|
2854
|
+
doScheduleEmbeddingGeneration() {
|
|
2855
|
+
const ctx = this.getEmbeddingSchedulerContext();
|
|
2856
|
+
scheduleEmbeddingGeneration(ctx, async () => {
|
|
2857
|
+
await triggerEmbeddingGeneration(this.getEmbeddingSchedulerContext());
|
|
2858
|
+
});
|
|
2859
|
+
}
|
|
2860
|
+
/**
|
|
2861
|
+
* Query the graph
|
|
2862
|
+
*/
|
|
2863
|
+
async queryGraph(query) {
|
|
2864
|
+
const cacheKey = QueryCacheManager.createKey(query);
|
|
2865
|
+
const cached = this.cacheManager.get(cacheKey);
|
|
2866
|
+
if (cached) {
|
|
2867
|
+
log.t("INDEXER", "cache_hit_query");
|
|
2868
|
+
return cached;
|
|
2869
|
+
}
|
|
2870
|
+
log.t("INDEXER", "exec_query");
|
|
2871
|
+
const result = await this.graphStorage.executeQuery(query);
|
|
2872
|
+
this.cacheManager.set(cacheKey, result);
|
|
2873
|
+
return result;
|
|
2874
|
+
}
|
|
2875
|
+
/**
|
|
2876
|
+
* Query subgraph for an entity
|
|
2877
|
+
*/
|
|
2878
|
+
async querySubgraph(entityId, depth) {
|
|
2879
|
+
const cacheKey = QueryCacheManager.createKey({ entityId, depth });
|
|
2880
|
+
const cached = this.cacheManager.get(cacheKey);
|
|
2881
|
+
if (cached) {
|
|
2882
|
+
log.t("INDEXER", "cache_hit_subgraph");
|
|
2883
|
+
return cached;
|
|
2884
|
+
}
|
|
2885
|
+
log.t("INDEXER", "get_subgraph", { entityId, depth });
|
|
2886
|
+
const result = await this.graphStorage.getSubgraph(entityId, depth);
|
|
2887
|
+
this.cacheManager.set(cacheKey, result);
|
|
2888
|
+
return result;
|
|
2889
|
+
}
|
|
2890
|
+
/**
|
|
2891
|
+
* Handle incoming messages
|
|
2892
|
+
*/
|
|
2893
|
+
async handleMessage(message) {
|
|
2894
|
+
log.d("INDEXER", "recv_msg", { type: message.type, from: message.from });
|
|
2895
|
+
switch (message.type) {
|
|
2896
|
+
case "index:request": {
|
|
2897
|
+
const task = {
|
|
2898
|
+
id: message.id,
|
|
2899
|
+
type: "index:entities",
|
|
2900
|
+
priority: 5,
|
|
2901
|
+
payload: message.payload,
|
|
2902
|
+
createdAt: Date.now()
|
|
2903
|
+
};
|
|
2904
|
+
await this.process(task);
|
|
2905
|
+
break;
|
|
2906
|
+
}
|
|
2907
|
+
case "query:request": {
|
|
2908
|
+
const queryTask = {
|
|
2909
|
+
id: message.id,
|
|
2910
|
+
type: "query:graph",
|
|
2911
|
+
priority: 8,
|
|
2912
|
+
payload: message.payload,
|
|
2913
|
+
createdAt: Date.now()
|
|
2914
|
+
};
|
|
2915
|
+
const result = await this.process(queryTask);
|
|
2916
|
+
await this.send({
|
|
2917
|
+
id: nanoid(12),
|
|
2918
|
+
from: this.id,
|
|
2919
|
+
to: message.from,
|
|
2920
|
+
type: "query:response",
|
|
2921
|
+
payload: result,
|
|
2922
|
+
timestamp: Date.now(),
|
|
2923
|
+
correlationId: message.id
|
|
2924
|
+
});
|
|
2925
|
+
break;
|
|
2926
|
+
}
|
|
2927
|
+
default:
|
|
2928
|
+
log.w("INDEXER", "unknown_msg_type", { type: message.type });
|
|
2929
|
+
}
|
|
2930
|
+
}
|
|
2931
|
+
/**
|
|
2932
|
+
* Get Git event context for a specific project path
|
|
2933
|
+
*/
|
|
2934
|
+
getGitEventContextForProject(projectPath) {
|
|
2935
|
+
return {
|
|
2936
|
+
agentId: this.id,
|
|
2937
|
+
currentRepositoryPath: projectPath,
|
|
2938
|
+
branchManager: this.branchManager
|
|
2939
|
+
};
|
|
2940
|
+
}
|
|
2941
|
+
/**
|
|
2942
|
+
* Get Git event context for extracted handlers (uses current project)
|
|
2943
|
+
*/
|
|
2944
|
+
getGitEventContext() {
|
|
2945
|
+
return this.getGitEventContextForProject(this.currentRepositoryPath);
|
|
2946
|
+
}
|
|
2947
|
+
/**
|
|
2948
|
+
* Get or create a GitWatcher for a specific project path.
|
|
2949
|
+
* Each project gets its own independent watcher so multi-session doesn't interfere.
|
|
2950
|
+
*/
|
|
2951
|
+
getOrCreateWatcher(projectPath) {
|
|
2952
|
+
const appConfig = getConfig();
|
|
2953
|
+
if (!appConfig.git?.enabled || !appConfig.git.watchBranchChanges || !this.branchManager) {
|
|
2954
|
+
return null;
|
|
2955
|
+
}
|
|
2956
|
+
const existing = this.gitWatchers.get(projectPath);
|
|
2957
|
+
if (existing) {
|
|
2958
|
+
return existing;
|
|
2959
|
+
}
|
|
2960
|
+
const watcher = new GitWatcher({
|
|
2961
|
+
enabled: true,
|
|
2962
|
+
pollIntervalMs: appConfig.git.pollIntervalMs || 5e3,
|
|
2963
|
+
autoReindex: appConfig.git.autoReindex ?? true,
|
|
2964
|
+
watchUncommitted: appConfig.git.watchUncommitted ?? true,
|
|
2965
|
+
uncommittedPollIntervalMs: appConfig.git.uncommittedPollIntervalMs || 1e4,
|
|
2966
|
+
includeUntracked: appConfig.git.includeUntracked ?? true,
|
|
2967
|
+
debounceMs: appConfig.git.debounceMs ?? 6e4,
|
|
2968
|
+
bulkModeThreshold: appConfig.git.bulkModeThreshold ?? 1e3
|
|
2969
|
+
});
|
|
2970
|
+
const ctx = () => this.getGitEventContextForProject(projectPath);
|
|
2971
|
+
watcher.onBranchChange(async (newBranch, oldBranch) => {
|
|
2972
|
+
await handleBranchChange(newBranch, oldBranch, ctx());
|
|
2973
|
+
});
|
|
2974
|
+
watcher.onUncommittedChange(async (files) => {
|
|
2975
|
+
await handleUncommittedChanges(files, ctx());
|
|
2976
|
+
});
|
|
2977
|
+
watcher.onDebouncedChange(async (files, bulkMode) => {
|
|
2978
|
+
await handleDebouncedEmbeddingGeneration(files, bulkMode, ctx());
|
|
2979
|
+
});
|
|
2980
|
+
this.gitWatchers.set(projectPath, watcher);
|
|
2981
|
+
watcher.startWatching(projectPath);
|
|
2982
|
+
log.i("INDEXER", "git_watcher_started", { repository: projectPath, totalWatchers: this.gitWatchers.size });
|
|
2983
|
+
return watcher;
|
|
2984
|
+
}
|
|
2985
|
+
/**
|
|
2986
|
+
* Get BranchManager instance
|
|
2987
|
+
*/
|
|
2988
|
+
getBranchManager() {
|
|
2989
|
+
return this.branchManager;
|
|
2990
|
+
}
|
|
2991
|
+
/**
|
|
2992
|
+
* Get GitWatcher instance for current or specific project
|
|
2993
|
+
*/
|
|
2994
|
+
getGitWatcher(projectPath) {
|
|
2995
|
+
const path = projectPath || this.currentRepositoryPath;
|
|
2996
|
+
return path ? this.gitWatchers.get(path) ?? null : null;
|
|
2997
|
+
}
|
|
2998
|
+
/**
|
|
2999
|
+
* Get FileWatcher status for diagnostics
|
|
3000
|
+
*/
|
|
3001
|
+
getFileWatcherStatus() {
|
|
3002
|
+
if (!this.fileWatcher) {
|
|
3003
|
+
return { exists: false, lastError: this.lastFileWatcherError };
|
|
3004
|
+
}
|
|
3005
|
+
return {
|
|
3006
|
+
exists: true,
|
|
3007
|
+
status: this.fileWatcher.getStatus(),
|
|
3008
|
+
lastError: null
|
|
3009
|
+
};
|
|
3010
|
+
}
|
|
3011
|
+
/**
|
|
3012
|
+
* Set current repository path and start watching if Git is enabled
|
|
3013
|
+
*/
|
|
3014
|
+
async setRepositoryPath(path) {
|
|
3015
|
+
this.currentRepositoryPath = path;
|
|
3016
|
+
this.getOrCreateWatcher(path);
|
|
3017
|
+
log.d("INDEXER", "creating_filewatcher", { path });
|
|
3018
|
+
try {
|
|
3019
|
+
if (this.fileWatcher) {
|
|
3020
|
+
log.d("INDEXER", "stopping_old_filewatcher");
|
|
3021
|
+
await this.fileWatcher.stop();
|
|
3022
|
+
this.fileWatcher = null;
|
|
3023
|
+
}
|
|
3024
|
+
const appConfig = getConfig();
|
|
3025
|
+
this.fileWatcher = await createFileWatcher({
|
|
3026
|
+
rootDir: path,
|
|
3027
|
+
include: [
|
|
3028
|
+
"**/*.ts",
|
|
3029
|
+
"**/*.tsx",
|
|
3030
|
+
"**/*.js",
|
|
3031
|
+
"**/*.jsx",
|
|
3032
|
+
"**/*.py",
|
|
3033
|
+
"**/*.go",
|
|
3034
|
+
"**/*.rs",
|
|
3035
|
+
"**/*.java",
|
|
3036
|
+
"**/*.kt",
|
|
3037
|
+
"**/*.cpp",
|
|
3038
|
+
"**/*.c",
|
|
3039
|
+
"**/*.h",
|
|
3040
|
+
"**/*.hpp"
|
|
3041
|
+
],
|
|
3042
|
+
exclude: [
|
|
3043
|
+
"**/node_modules/**",
|
|
3044
|
+
"**/.git/**",
|
|
3045
|
+
"**/dist/**",
|
|
3046
|
+
"**/build/**",
|
|
3047
|
+
"**/.ultracode/**",
|
|
3048
|
+
"**/coverage/**",
|
|
3049
|
+
"**/__pycache__/**",
|
|
3050
|
+
"**/venv/**",
|
|
3051
|
+
"**/.venv/**"
|
|
3052
|
+
],
|
|
3053
|
+
debounceMs: 100,
|
|
3054
|
+
bulkThreshold: appConfig.git?.bulkModeThreshold ?? 1e3
|
|
3055
|
+
});
|
|
3056
|
+
this.fileWatcher.on("change", (events, bulkMode) => {
|
|
3057
|
+
this.handleFileWatcherChanges(events, bulkMode).catch((err) => {
|
|
3058
|
+
log.e("INDEXER", "FileWatcher change handler error", { error: err.message });
|
|
3059
|
+
});
|
|
3060
|
+
});
|
|
3061
|
+
this.fileWatcher.on("error", (err) => {
|
|
3062
|
+
log.e("INDEXER", "FileWatcher error", { error: err.message });
|
|
3063
|
+
});
|
|
3064
|
+
log.i("INDEXER", "Started FileWatcher", { repository: path });
|
|
3065
|
+
this.lastFileWatcherError = null;
|
|
3066
|
+
} catch (err) {
|
|
3067
|
+
this.lastFileWatcherError = err.message;
|
|
3068
|
+
log.w("INDEXER", "Failed to start FileWatcher, using GitWatcher only", {
|
|
3069
|
+
error: this.lastFileWatcherError
|
|
3070
|
+
});
|
|
3071
|
+
}
|
|
3072
|
+
}
|
|
3073
|
+
/**
|
|
3074
|
+
* Handle file changes detected by FileWatcher
|
|
3075
|
+
* Triggers incremental reindexing and debounced embedding generation
|
|
3076
|
+
*/
|
|
3077
|
+
async handleFileWatcherChanges(events, bulkMode) {
|
|
3078
|
+
if (events.length === 0) return;
|
|
3079
|
+
const changedFiles = events.filter((e) => e.type === "add" || e.type === "change").map((e) => e.path);
|
|
3080
|
+
const deletedFiles = events.filter((e) => e.type === "unlink").map((e) => e.path);
|
|
3081
|
+
log.d("INDEXER", "FileWatcher detected changes", {
|
|
3082
|
+
changed: changedFiles.length,
|
|
3083
|
+
deleted: deletedFiles.length,
|
|
3084
|
+
bulkMode
|
|
3085
|
+
});
|
|
3086
|
+
if (changedFiles.length > 0) {
|
|
3087
|
+
await handleUncommittedChanges(changedFiles, this.getGitEventContext());
|
|
3088
|
+
}
|
|
3089
|
+
if (deletedFiles.length > 0) {
|
|
3090
|
+
log.d("INDEXER", "Detected deleted files (cleaned on reindex)", {
|
|
3091
|
+
count: deletedFiles.length,
|
|
3092
|
+
files: deletedFiles.slice(0, 5)
|
|
3093
|
+
});
|
|
3094
|
+
}
|
|
3095
|
+
if (!bulkMode) {
|
|
3096
|
+
this.doScheduleEmbeddingGeneration();
|
|
3097
|
+
} else {
|
|
3098
|
+
await handleDebouncedEmbeddingGeneration(changedFiles, bulkMode, this.getGitEventContext());
|
|
3099
|
+
}
|
|
3100
|
+
}
|
|
3101
|
+
/**
|
|
3102
|
+
* Shutdown the indexer agent
|
|
3103
|
+
*/
|
|
3104
|
+
async onShutdown() {
|
|
3105
|
+
log.i("INDEXER", "Shutting down...");
|
|
3106
|
+
if (this.idleFlushAbort) {
|
|
3107
|
+
this.idleFlushAbort.abort();
|
|
3108
|
+
this.idleFlushAbort = null;
|
|
3109
|
+
}
|
|
3110
|
+
if (this.pendingFilesCount > 0) {
|
|
3111
|
+
log.d("INDEXER", "shutdown_flush_pending", { pending: this.pendingFilesCount });
|
|
3112
|
+
try {
|
|
3113
|
+
await this.flushPendingBatch();
|
|
3114
|
+
} catch (err) {
|
|
3115
|
+
log.w("INDEXER", "shutdown_flush_failed", { error: err.message });
|
|
3116
|
+
}
|
|
3117
|
+
}
|
|
3118
|
+
if (this.fileWatcher) {
|
|
3119
|
+
try {
|
|
3120
|
+
await this.fileWatcher.stop();
|
|
3121
|
+
this.fileWatcher = null;
|
|
3122
|
+
log.d("INDEXER", "FileWatcher stopped");
|
|
3123
|
+
} catch (err) {
|
|
3124
|
+
log.w("INDEXER", "Error stopping FileWatcher", { error: err.message });
|
|
3125
|
+
}
|
|
3126
|
+
}
|
|
3127
|
+
for (const [path, watcher] of this.gitWatchers) {
|
|
3128
|
+
watcher.stopWatching();
|
|
3129
|
+
log.d("INDEXER", "git_watcher_stopped", { repository: path });
|
|
3130
|
+
}
|
|
3131
|
+
this.gitWatchers.clear();
|
|
3132
|
+
try {
|
|
3133
|
+
for (const id of this.subscriptionIds) {
|
|
3134
|
+
knowledgeBus.unsubscribe(id);
|
|
3135
|
+
}
|
|
3136
|
+
} catch {
|
|
3137
|
+
}
|
|
3138
|
+
if (this.ready) {
|
|
3139
|
+
try {
|
|
3140
|
+
await this.graphStorage.analyze();
|
|
3141
|
+
} catch (e) {
|
|
3142
|
+
log.w("INDEXER", "shutdown_analyze_skip", { err: e.message });
|
|
3143
|
+
}
|
|
3144
|
+
}
|
|
3145
|
+
try {
|
|
3146
|
+
this.cacheManager?.clear();
|
|
3147
|
+
} catch {
|
|
3148
|
+
}
|
|
3149
|
+
log.i("INDEXER", "shutdown_done", this.indexingStats);
|
|
3150
|
+
}
|
|
3151
|
+
/**
|
|
3152
|
+
* Get indexing statistics
|
|
3153
|
+
*/
|
|
3154
|
+
getIndexingStats() {
|
|
3155
|
+
return { ...this.indexingStats };
|
|
3156
|
+
}
|
|
3157
|
+
/**
|
|
3158
|
+
* Get storage metrics
|
|
3159
|
+
*/
|
|
3160
|
+
async getStorageMetrics() {
|
|
3161
|
+
return await this.graphStorage.getMetrics();
|
|
3162
|
+
}
|
|
3163
|
+
/**
|
|
3164
|
+
* Perform maintenance operations
|
|
3165
|
+
*/
|
|
3166
|
+
async performMaintenance() {
|
|
3167
|
+
log.i("INDEXER", "maint_start");
|
|
3168
|
+
await this.graphStorage.vacuum();
|
|
3169
|
+
await this.graphStorage.analyze();
|
|
3170
|
+
this.cacheManager.prune();
|
|
3171
|
+
const avgTime = this.indexingStats.totalIndexTime / Math.max(1, this.indexingStats.filesProcessed);
|
|
3172
|
+
this.batchOps.optimizeBatchSize(avgTime);
|
|
3173
|
+
log.i("INDEXER", "maint_done");
|
|
3174
|
+
}
|
|
3175
|
+
};
|
|
3176
|
+
|
|
3177
|
+
export { IndexerAgent };
|
|
3178
|
+
//# sourceMappingURL=chunk-RGP5UVQ7.js.map
|
|
3179
|
+
//# sourceMappingURL=chunk-RGP5UVQ7.js.map
|