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.
Files changed (57) hide show
  1. package/dist/chunks/analysis-tool-handlers-H2RXLDPX.js +817 -0
  2. package/dist/chunks/analysis-tool-handlers-RJZAR6VT.js +817 -0
  3. package/dist/chunks/analysis-tool-handlers-Z2RF24T7.js +13 -0
  4. package/dist/chunks/autodoc-tool-handlers-CV5JEQUA.js +1112 -0
  5. package/dist/chunks/autodoc-tool-handlers-EHTNCH6I.js +1112 -0
  6. package/dist/chunks/autodoc-tool-handlers-MECXQJ2K.js +138 -0
  7. package/dist/chunks/chaos-CO7TOBOJ.js +18 -0
  8. package/dist/chunks/chaos-VM2PXERO.js +1573 -0
  9. package/dist/chunks/chaos-W3XRVJ7K.js +1564 -0
  10. package/dist/chunks/chunk-6K37BWK5.js +439 -0
  11. package/dist/chunks/chunk-EALTCYHZ.js +10 -0
  12. package/dist/chunks/chunk-FTBE7VMY.js +316 -0
  13. package/dist/chunks/chunk-KBW6LRQP.js +322 -0
  14. package/dist/chunks/chunk-NKUHX4CU.js +5 -0
  15. package/dist/chunks/chunk-NZFF4DQ4.js +3179 -0
  16. package/dist/chunks/chunk-RGP5UVQ7.js +3179 -0
  17. package/dist/chunks/chunk-RMZXFGQZ.js +322 -0
  18. package/dist/chunks/chunk-UG44F23Y.js +316 -0
  19. package/dist/chunks/chunk-V2SCB5H5.js +4403 -0
  20. package/dist/chunks/chunk-V6JAQNM3.js +1 -0
  21. package/dist/chunks/chunk-XFGXM4CR.js +4403 -0
  22. package/dist/chunks/dev-agent-JVIGBMHQ.js +1 -0
  23. package/dist/chunks/dev-agent-TRVP5U6N.js +1624 -0
  24. package/dist/chunks/dev-agent-Y5G5WKQ4.js +1624 -0
  25. package/dist/chunks/graph-storage-factory-AYZ57YSL.js +13 -0
  26. package/dist/chunks/graph-storage-factory-GTAIJEI5.js +1 -0
  27. package/dist/chunks/graph-storage-factory-T2WO5QVG.js +13 -0
  28. package/dist/chunks/incremental-updater-KDIQGAUU.js +14 -0
  29. package/dist/chunks/incremental-updater-OJRSTO3Q.js +1 -0
  30. package/dist/chunks/incremental-updater-SBEBH7KF.js +14 -0
  31. package/dist/chunks/indexer-agent-H3QIEL3Z.js +21 -0
  32. package/dist/chunks/indexer-agent-KHF5JMV7.js +21 -0
  33. package/dist/chunks/indexer-agent-SHJD6Z77.js +1 -0
  34. package/dist/chunks/indexing-pipeline-J6Z4BHKF.js +1 -0
  35. package/dist/chunks/indexing-pipeline-OY3337QN.js +249 -0
  36. package/dist/chunks/indexing-pipeline-WCXIDMAP.js +249 -0
  37. package/dist/chunks/merge-agent-LSUBDJB2.js +2481 -0
  38. package/dist/chunks/merge-agent-MJEW3HWU.js +2481 -0
  39. package/dist/chunks/merge-agent-O45OXF33.js +11 -0
  40. package/dist/chunks/merge-tool-handlers-BDSVNQVZ.js +277 -0
  41. package/dist/chunks/merge-tool-handlers-HP7DRBXJ.js +1 -0
  42. package/dist/chunks/merge-tool-handlers-RUJAKE3D.js +277 -0
  43. package/dist/chunks/pattern-tool-handlers-L62W3CXR.js +1549 -0
  44. package/dist/chunks/pattern-tool-handlers-SAHX2CVW.js +13 -0
  45. package/dist/chunks/query-agent-3TWDFIMT.js +191 -0
  46. package/dist/chunks/query-agent-HXQ3BMMF.js +191 -0
  47. package/dist/chunks/query-agent-USMC2GNG.js +1 -0
  48. package/dist/chunks/semantic-agent-MQCAWIAB.js +6381 -0
  49. package/dist/chunks/semantic-agent-NDGR3NAK.js +6381 -0
  50. package/dist/chunks/semantic-agent-S4ZL6GZC.js +137 -0
  51. package/dist/index.js +17 -17
  52. package/dist/roslyn-addon/.build-hash +1 -1
  53. package/dist/roslyn-addon/ILGPU.Algorithms.dll +0 -0
  54. package/dist/roslyn-addon/ILGPU.dll +0 -0
  55. package/dist/roslyn-addon/UltraCode.CSharp.deps.json +35 -0
  56. package/dist/roslyn-addon/UltraCode.CSharp.dll +0 -0
  57. 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-V2SCB5H5.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-NZFF4DQ4.js.map
3179
+ //# sourceMappingURL=chunk-NZFF4DQ4.js.map