grepmax 0.17.23 → 0.18.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.
@@ -0,0 +1,484 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
36
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
37
+ return new (P || (P = Promise))(function (resolve, reject) {
38
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
39
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
40
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
41
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
42
+ });
43
+ };
44
+ var __asyncValues = (this && this.__asyncValues) || function (o) {
45
+ if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined.");
46
+ var m = o[Symbol.asyncIterator], i;
47
+ return m ? m.call(o) : (o = typeof __values === "function" ? __values(o) : o[Symbol.iterator](), i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () { return this; }, i);
48
+ function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; }
49
+ function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); }
50
+ };
51
+ Object.defineProperty(exports, "__esModule", { value: true });
52
+ exports.WatcherManager = void 0;
53
+ const watcher = __importStar(require("@parcel/watcher"));
54
+ const fs = __importStar(require("node:fs"));
55
+ const path = __importStar(require("node:path"));
56
+ const batch_processor_1 = require("../index/batch-processor");
57
+ const watcher_1 = require("../index/watcher");
58
+ const logger_1 = require("../utils/logger");
59
+ const project_registry_1 = require("../utils/project-registry");
60
+ const watcher_store_1 = require("../utils/watcher-store");
61
+ // Watcher health windows used for FSEvents auto-recovery.
62
+ const FSEVENTS_RECOVERY_INTERVAL_MS = 60 * 60 * 1000; // try recovery hourly
63
+ const FSEVENTS_HEALTH_WINDOW_MS = 5 * 60 * 1000; // 5 min of quiet = "healthy"
64
+ /**
65
+ * Owns per-project file watching: @parcel/watcher subscriptions, FSEvents
66
+ * overflow recovery, poll-mode fallback, and the offline catchup scan. Holds the
67
+ * watcher health/state maps; shares the `processors` and `subscriptions` maps
68
+ * with the daemon by reference so the streaming index ops and shutdown can still
69
+ * detach a project directly.
70
+ *
71
+ * Extracted from daemon.ts (Phase 2). Behavior-preserving — the daemon keeps
72
+ * thin watchProject/unwatchProject delegators over this manager.
73
+ */
74
+ class WatcherManager {
75
+ constructor(deps) {
76
+ this.deps = deps;
77
+ this.pendingOps = new Set();
78
+ this.watcherFailCount = new Map();
79
+ this.pollIntervals = new Map();
80
+ this.pollRecoveryTimers = new Map();
81
+ this.lastOverflowMs = new Map();
82
+ this.lastCatchupEndMs = new Map();
83
+ }
84
+ watchProject(root) {
85
+ return __awaiter(this, void 0, void 0, function* () {
86
+ if (this.deps.processors.has(root) || this.pendingOps.has(root))
87
+ return;
88
+ const vectorDb = this.deps.getVectorDb();
89
+ const metaCache = this.deps.getMetaCache();
90
+ if (!vectorDb || !metaCache)
91
+ return;
92
+ this.pendingOps.add(root);
93
+ const processor = new batch_processor_1.ProjectBatchProcessor({
94
+ projectRoot: root,
95
+ vectorDb,
96
+ metaCache,
97
+ onReindex: (files, ms) => __awaiter(this, void 0, void 0, function* () {
98
+ console.log(`[daemon:${path.basename(root)}] Reindexed ${files} file${files !== 1 ? "s" : ""} (${(ms / 1000).toFixed(1)}s)`);
99
+ // Update project registry so gmax status shows fresh data
100
+ const proj = (0, project_registry_1.getProject)(root);
101
+ if (proj) {
102
+ let chunkCount = proj.chunkCount;
103
+ try {
104
+ chunkCount = yield vectorDb.countRowsForPath(root);
105
+ }
106
+ catch (err) {
107
+ console.warn(`[daemon:${path.basename(root)}] Failed to query chunk count: ${err}`);
108
+ }
109
+ (0, project_registry_1.registerProject)(Object.assign(Object.assign({}, proj), { lastIndexed: new Date().toISOString(), chunkCount }));
110
+ }
111
+ // Back to watching after batch completes
112
+ (0, watcher_store_1.registerWatcher)({
113
+ pid: process.pid,
114
+ projectRoot: root,
115
+ startTime: Date.now(),
116
+ status: "watching",
117
+ lastHeartbeat: Date.now(),
118
+ lastReindex: Date.now(),
119
+ });
120
+ }),
121
+ onActivity: () => {
122
+ this.deps.touchActivity();
123
+ // Mark as syncing while processing
124
+ (0, watcher_store_1.registerWatcher)({
125
+ pid: process.pid,
126
+ projectRoot: root,
127
+ startTime: Date.now(),
128
+ status: "syncing",
129
+ lastHeartbeat: Date.now(),
130
+ });
131
+ },
132
+ });
133
+ this.deps.processors.set(root, processor);
134
+ // Subscribe with @parcel/watcher — native backend, no polling.
135
+ // If the kernel refuses (e.g. FSEvents slots stuck after a prior kill -9),
136
+ // fall straight through to poll mode. The retry/backoff path inside
137
+ // recoverWatcher is for transient overflows, not hard kernel-level
138
+ // subscribe failures, so we skip it on startup by priming failCount past
139
+ // MAX before invoking it.
140
+ try {
141
+ yield this.subscribeWatcher(root, processor);
142
+ }
143
+ catch (err) {
144
+ const name = path.basename(root);
145
+ console.error(`[daemon:${name}] Subscribe failed at startup (${err instanceof Error ? err.message : err}) — switching to poll mode`);
146
+ this.watcherFailCount.set(root, 1000); // > MAX_WATCHER_RETRIES
147
+ this.lastOverflowMs.set(root, Date.now());
148
+ this.recoverWatcher(root, processor);
149
+ }
150
+ (0, watcher_store_1.registerWatcher)({
151
+ pid: process.pid,
152
+ projectRoot: root,
153
+ startTime: Date.now(),
154
+ status: "watching",
155
+ lastHeartbeat: Date.now(),
156
+ });
157
+ // Catchup scan — find files changed while daemon was offline
158
+ this.catchupScan(root, processor).catch((err) => {
159
+ console.error(`[daemon:${path.basename(root)}] Catchup scan failed:`, err);
160
+ });
161
+ this.pendingOps.delete(root);
162
+ console.log(`[daemon] Watching ${root}`);
163
+ });
164
+ }
165
+ subscribeWatcher(root, processor) {
166
+ return __awaiter(this, void 0, void 0, function* () {
167
+ const name = path.basename(root);
168
+ // Unsubscribe existing watcher if any (e.g. during recovery)
169
+ const existingSub = this.deps.subscriptions.get(root);
170
+ if (existingSub) {
171
+ try {
172
+ yield existingSub.unsubscribe();
173
+ }
174
+ catch (_a) { }
175
+ this.deps.subscriptions.delete(root);
176
+ }
177
+ const sub = yield watcher.subscribe(root, (err, events) => {
178
+ var _a;
179
+ if (err) {
180
+ console.error(`[daemon:${name}] Watcher error:`, err);
181
+ this.recoverWatcher(root, processor);
182
+ return;
183
+ }
184
+ // Only reset fail counter after sustained health (5min since last overflow)
185
+ const lastOverflow = (_a = this.lastOverflowMs.get(root)) !== null && _a !== void 0 ? _a : 0;
186
+ if (Date.now() - lastOverflow > 5 * 60 * 1000) {
187
+ this.watcherFailCount.delete(root);
188
+ }
189
+ for (const event of events) {
190
+ processor.handleFileEvent(event.type === "delete" ? "unlink" : "change", event.path);
191
+ }
192
+ this.deps.touchActivity();
193
+ }, { ignore: watcher_1.WATCHER_IGNORE_GLOBS });
194
+ this.deps.subscriptions.set(root, sub);
195
+ });
196
+ }
197
+ recoverWatcher(root, processor) {
198
+ var _a;
199
+ const name = path.basename(root);
200
+ if (this.deps.getShuttingDown())
201
+ return;
202
+ // Debounce: avoid multiple overlapping recovery attempts
203
+ const recoveryKey = `recover:${root}`;
204
+ if (this.pendingOps.has(recoveryKey))
205
+ return;
206
+ this.pendingOps.add(recoveryKey);
207
+ const fails = ((_a = this.watcherFailCount.get(root)) !== null && _a !== void 0 ? _a : 0) + 1;
208
+ this.watcherFailCount.set(root, fails);
209
+ this.lastOverflowMs.set(root, Date.now());
210
+ const MAX_WATCHER_RETRIES = 3;
211
+ const POLL_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes
212
+ if (fails > MAX_WATCHER_RETRIES) {
213
+ // FSEvents can't handle this project — degrade to periodic catchup scans
214
+ // Always tear down the broken sub, even if poll mode is already active —
215
+ // this can happen if a recovery attempt resubscribed successfully then
216
+ // re-overflowed during the 5-min health window.
217
+ const sub = this.deps.subscriptions.get(root);
218
+ if (sub) {
219
+ sub.unsubscribe().catch(() => { });
220
+ this.deps.subscriptions.delete(root);
221
+ }
222
+ if (!this.pollIntervals.has(root)) {
223
+ console.error(`[daemon:${name}] FSEvents unreliable after ${fails} failures — switching to poll mode (${POLL_INTERVAL_MS / 60000}min interval)`);
224
+ // Run an immediate catchup, then schedule periodic ones
225
+ this.catchupScan(root, processor).catch((err) => {
226
+ console.error(`[daemon:${name}] Poll catchup failed:`, err);
227
+ });
228
+ const interval = setInterval(() => {
229
+ if (this.deps.getShuttingDown())
230
+ return;
231
+ this.deps.touchActivity();
232
+ this.catchupScan(root, processor).catch((err) => {
233
+ console.error(`[daemon:${name}] Poll catchup failed:`, err);
234
+ });
235
+ }, POLL_INTERVAL_MS);
236
+ this.pollIntervals.set(root, interval);
237
+ // Schedule periodic attempts to climb back to native FSEvents — after
238
+ // a transient burst (large git checkout, npm install) the kernel
239
+ // buffer often calms down within an hour.
240
+ this.schedulePollModeRecovery(root, processor);
241
+ (0, watcher_store_1.registerWatcher)({
242
+ pid: process.pid,
243
+ projectRoot: root,
244
+ startTime: Date.now(),
245
+ status: "watching",
246
+ lastHeartbeat: Date.now(),
247
+ });
248
+ }
249
+ this.pendingOps.delete(recoveryKey);
250
+ return;
251
+ }
252
+ // Backoff: wait before re-subscribing (3s, 6s, 12s)
253
+ const delayMs = 3000 * Math.pow(2, fails - 1);
254
+ console.error(`[daemon:${name}] Recovering watcher (attempt ${fails}/${MAX_WATCHER_RETRIES}, backoff ${delayMs}ms)...`);
255
+ setTimeout(() => {
256
+ if (this.deps.getShuttingDown()) {
257
+ this.pendingOps.delete(recoveryKey);
258
+ return;
259
+ }
260
+ (() => __awaiter(this, void 0, void 0, function* () {
261
+ var _a;
262
+ try {
263
+ yield this.subscribeWatcher(root, processor);
264
+ const lastCatchup = (_a = this.lastCatchupEndMs.get(root)) !== null && _a !== void 0 ? _a : 0;
265
+ const CATCHUP_COOLDOWN_MS = 60000;
266
+ if (Date.now() - lastCatchup < CATCHUP_COOLDOWN_MS) {
267
+ console.log(`[daemon:${name}] Skipping catchup scan (last completed ${Math.round((Date.now() - lastCatchup) / 1000)}s ago)`);
268
+ }
269
+ else {
270
+ yield this.catchupScan(root, processor);
271
+ }
272
+ console.log(`[daemon:${name}] Watcher recovered`);
273
+ }
274
+ catch (err) {
275
+ console.error(`[daemon:${name}] Watcher recovery failed:`, err);
276
+ }
277
+ finally {
278
+ this.pendingOps.delete(recoveryKey);
279
+ }
280
+ }))();
281
+ }, delayMs);
282
+ }
283
+ /**
284
+ * Once a project has fallen back to poll mode, periodically try to upgrade
285
+ * back to native FSEvents. The buffer overflows that triggered the fallback
286
+ * are usually transient (big git checkout, npm install, build output) — no
287
+ * point staying in 5-min poll mode forever.
288
+ */
289
+ schedulePollModeRecovery(root, processor) {
290
+ if (this.pollRecoveryTimers.has(root))
291
+ return;
292
+ const name = path.basename(root);
293
+ const timer = setInterval(() => {
294
+ if (this.deps.getShuttingDown())
295
+ return;
296
+ // Skip if a watcher recovery is already in flight or we're not in poll mode anymore.
297
+ if (!this.pollIntervals.has(root)) {
298
+ const t = this.pollRecoveryTimers.get(root);
299
+ if (t)
300
+ clearInterval(t);
301
+ this.pollRecoveryTimers.delete(root);
302
+ return;
303
+ }
304
+ if (this.pendingOps.has(`recover:${root}`))
305
+ return;
306
+ void (() => __awaiter(this, void 0, void 0, function* () {
307
+ var _a;
308
+ console.log(`[daemon:${name}] Attempting to leave poll mode and reattach FSEvents...`);
309
+ try {
310
+ // Reset failure counter so subscribeWatcher's error path treats this
311
+ // as a fresh start. If it fails again, we'll fall right back into
312
+ // poll mode via the same recoverWatcher path.
313
+ this.watcherFailCount.delete(root);
314
+ yield this.subscribeWatcher(root, processor);
315
+ // Wait one health window — if the new subscription survives without
316
+ // another overflow, we consider it recovered and tear down poll mode.
317
+ yield new Promise((r) => setTimeout(r, FSEVENTS_HEALTH_WINDOW_MS));
318
+ if (this.deps.getShuttingDown())
319
+ return;
320
+ const lastOverflow = (_a = this.lastOverflowMs.get(root)) !== null && _a !== void 0 ? _a : 0;
321
+ if (Date.now() - lastOverflow < FSEVENTS_HEALTH_WINDOW_MS) {
322
+ console.log(`[daemon:${name}] FSEvents recovery aborted — fresh overflow within health window, staying in poll mode`);
323
+ return; // recoverWatcher will have re-armed poll mode if needed
324
+ }
325
+ // Healthy — drop poll mode.
326
+ const pollInterval = this.pollIntervals.get(root);
327
+ if (pollInterval) {
328
+ clearInterval(pollInterval);
329
+ this.pollIntervals.delete(root);
330
+ }
331
+ const recoveryTimer = this.pollRecoveryTimers.get(root);
332
+ if (recoveryTimer) {
333
+ clearInterval(recoveryTimer);
334
+ this.pollRecoveryTimers.delete(root);
335
+ }
336
+ console.log(`[daemon:${name}] FSEvents recovered — poll mode disabled`);
337
+ }
338
+ catch (err) {
339
+ console.error(`[daemon:${name}] Poll-mode recovery attempt failed:`, err);
340
+ }
341
+ }))();
342
+ }, FSEVENTS_RECOVERY_INTERVAL_MS);
343
+ timer.unref();
344
+ this.pollRecoveryTimers.set(root, timer);
345
+ }
346
+ catchupScan(root, processor) {
347
+ return __awaiter(this, void 0, void 0, function* () {
348
+ var _a, e_1, _b, _c;
349
+ const { walk } = yield Promise.resolve().then(() => __importStar(require("../index/walker")));
350
+ const { INDEXABLE_EXTENSIONS, MAX_FILE_SIZE_BYTES } = yield Promise.resolve().then(() => __importStar(require("../../config")));
351
+ const { isFileCached } = yield Promise.resolve().then(() => __importStar(require("../utils/cache-check")));
352
+ const metaCache = this.deps.getMetaCache();
353
+ const rootPrefix = root.endsWith("/") ? root : `${root}/`;
354
+ const cachedPaths = yield metaCache.getKeysWithPrefix(rootPrefix);
355
+ const seenPaths = new Set();
356
+ let queued = 0;
357
+ let skipped = 0;
358
+ let debugSamples = 0;
359
+ try {
360
+ for (var _d = true, _e = __asyncValues(walk(root, {
361
+ additionalPatterns: ["**/.git/**", "**/.gmax/**"],
362
+ })), _f; _f = yield _e.next(), _a = _f.done, !_a; _d = true) {
363
+ _c = _f.value;
364
+ _d = false;
365
+ const relPath = _c;
366
+ const absPath = path.join(root, relPath);
367
+ const ext = path.extname(absPath).toLowerCase();
368
+ const bn = path.basename(absPath).toLowerCase();
369
+ if (!INDEXABLE_EXTENSIONS.has(ext) && !INDEXABLE_EXTENSIONS.has(bn))
370
+ continue;
371
+ seenPaths.add(absPath);
372
+ try {
373
+ const stats = yield fs.promises.stat(absPath);
374
+ // Skip files that are too large or empty — they'll never be indexed
375
+ if (stats.size === 0 || stats.size > MAX_FILE_SIZE_BYTES)
376
+ continue;
377
+ const cached = metaCache.get(absPath);
378
+ if (!isFileCached(cached, stats)) {
379
+ // Fast path: if only mtime changed but size is identical and we have a hash,
380
+ // just verify the hash in-process instead of sending to a worker.
381
+ if (cached && cached.hash && cached.size === stats.size) {
382
+ const { computeBufferHash } = yield Promise.resolve().then(() => __importStar(require("../utils/file-utils")));
383
+ const buf = yield fs.promises.readFile(absPath);
384
+ const hash = computeBufferHash(buf);
385
+ if (hash === cached.hash) {
386
+ // Content unchanged — update mtime in cache and skip worker
387
+ metaCache.put(absPath, Object.assign(Object.assign({}, cached), { mtimeMs: stats.mtimeMs }));
388
+ skipped++;
389
+ continue;
390
+ }
391
+ }
392
+ // Debug: log first few misses to diagnose re-queue loops
393
+ if (debugSamples < 5) {
394
+ (0, logger_1.debug)("catchup", `miss ${relPath}: cached=${cached ? `mtime=${Math.trunc(cached.mtimeMs)} size=${cached.size}` : "null"} stat=mtime=${Math.trunc(stats.mtimeMs)} size=${stats.size}`);
395
+ debugSamples++;
396
+ }
397
+ processor.handleFileEvent("change", absPath);
398
+ queued++;
399
+ // Throttle: pause periodically during large catchup scans to let the
400
+ // batch processor drain and compaction run between bursts.
401
+ if (queued % 500 === 0) {
402
+ (0, logger_1.debug)("catchup", `${path.basename(root)}: throttle pause at ${queued} queued`);
403
+ yield new Promise(r => setTimeout(r, 5000));
404
+ }
405
+ }
406
+ else {
407
+ skipped++;
408
+ }
409
+ }
410
+ catch (_g) { }
411
+ }
412
+ }
413
+ catch (e_1_1) { e_1 = { error: e_1_1 }; }
414
+ finally {
415
+ try {
416
+ if (!_d && !_a && (_b = _e.return)) yield _b.call(_e);
417
+ }
418
+ finally { if (e_1) throw e_1.error; }
419
+ }
420
+ (0, logger_1.debug)("catchup", `${path.basename(root)}: ${queued} queued, ${skipped} skipped (cached ok), ${seenPaths.size} total`);
421
+ // Purge files deleted while daemon was offline
422
+ let purged = 0;
423
+ for (const cachedPath of cachedPaths) {
424
+ if (!seenPaths.has(cachedPath)) {
425
+ processor.handleFileEvent("unlink", cachedPath);
426
+ purged++;
427
+ }
428
+ }
429
+ if (queued > 0 || purged > 0) {
430
+ const parts = [];
431
+ if (queued > 0)
432
+ parts.push(`${queued} changed`);
433
+ if (purged > 0)
434
+ parts.push(`${purged} deleted`);
435
+ console.log(`[daemon:${path.basename(root)}] Catchup: ${parts.join(", ")} file(s) while offline`);
436
+ }
437
+ this.lastCatchupEndMs.set(root, Date.now());
438
+ });
439
+ }
440
+ unwatchProject(root) {
441
+ return __awaiter(this, void 0, void 0, function* () {
442
+ const processor = this.deps.processors.get(root);
443
+ if (!processor)
444
+ return;
445
+ yield processor.close();
446
+ const sub = this.deps.subscriptions.get(root);
447
+ if (sub) {
448
+ yield sub.unsubscribe();
449
+ this.deps.subscriptions.delete(root);
450
+ }
451
+ this.deps.processors.delete(root);
452
+ this.deps.evictSearcher(root);
453
+ this.lastOverflowMs.delete(root);
454
+ this.lastCatchupEndMs.delete(root);
455
+ (0, watcher_store_1.unregisterWatcherByRoot)(root);
456
+ console.log(`[daemon] Unwatched ${root}`);
457
+ });
458
+ }
459
+ /**
460
+ * Stop all poll intervals + their FSEvents recovery probes and unsubscribe
461
+ * every watcher. Called from the daemon's shutdown after the worker pool is
462
+ * destroyed (the subscriptions map is shared, so it is cleared here).
463
+ */
464
+ teardown() {
465
+ return __awaiter(this, void 0, void 0, function* () {
466
+ for (const interval of this.pollIntervals.values()) {
467
+ clearInterval(interval);
468
+ }
469
+ this.pollIntervals.clear();
470
+ for (const interval of this.pollRecoveryTimers.values()) {
471
+ clearInterval(interval);
472
+ }
473
+ this.pollRecoveryTimers.clear();
474
+ for (const sub of this.deps.subscriptions.values()) {
475
+ try {
476
+ yield sub.unsubscribe();
477
+ }
478
+ catch (_a) { }
479
+ }
480
+ this.deps.subscriptions.clear();
481
+ });
482
+ }
483
+ }
484
+ exports.WatcherManager = WatcherManager;
@@ -17,7 +17,11 @@ class GraphBuilder {
17
17
  constructor(db, pathPrefix, excludePrefixes) {
18
18
  this.db = db;
19
19
  // Normalize to ensure trailing slash for LIKE queries
20
- this.pathPrefix = pathPrefix ? (pathPrefix.endsWith("/") ? pathPrefix : `${pathPrefix}/`) : undefined;
20
+ this.pathPrefix = pathPrefix
21
+ ? pathPrefix.endsWith("/")
22
+ ? pathPrefix
23
+ : `${pathPrefix}/`
24
+ : undefined;
21
25
  this.excludePrefixes = (excludePrefixes !== null && excludePrefixes !== void 0 ? excludePrefixes : []).map((p) => p.endsWith("/") ? p : `${p}/`);
22
26
  }
23
27
  scopeWhere(condition) {
@@ -37,14 +41,22 @@ class GraphBuilder {
37
41
  return __awaiter(this, void 0, void 0, function* () {
38
42
  const table = yield this.db.ensureTable();
39
43
  const escaped = (0, filter_builder_1.escapeSqlString)(symbol);
40
- // Find chunks where referenced_symbols contains the symbol
44
+ // Find chunks that reference the symbol from a call position
45
+ // (referenced_symbols) OR a type position (type_referenced_symbols). The
46
+ // two are stored separately so type edges never inflate the call-edge count
47
+ // used for ranking; navigation unions them.
41
48
  const rows = yield table
42
49
  .query()
43
50
  .select([
44
- "path", "start_line", "defined_symbols", "referenced_symbols",
45
- "role", "parent_symbol", "complexity",
51
+ "path",
52
+ "start_line",
53
+ "defined_symbols",
54
+ "referenced_symbols",
55
+ "role",
56
+ "parent_symbol",
57
+ "complexity",
46
58
  ])
47
- .where(this.scopeWhere(`array_contains(referenced_symbols, '${escaped}')`))
59
+ .where(this.scopeWhere(`(array_contains(referenced_symbols, '${escaped}') OR array_contains(type_referenced_symbols, '${escaped}'))`))
48
60
  .limit(100)
49
61
  .toArray();
50
62
  return rows.map((row) => this.mapRowToNode(row, symbol, "caller"));
@@ -82,8 +94,13 @@ class GraphBuilder {
82
94
  const centerRows = yield table
83
95
  .query()
84
96
  .select([
85
- "path", "start_line", "defined_symbols", "referenced_symbols",
86
- "role", "parent_symbol", "complexity",
97
+ "path",
98
+ "start_line",
99
+ "defined_symbols",
100
+ "referenced_symbols",
101
+ "role",
102
+ "parent_symbol",
103
+ "complexity",
87
104
  ])
88
105
  .where(this.scopeWhere(`array_contains(defined_symbols, '${escaped}')`))
89
106
  .limit(1)
@@ -165,7 +182,12 @@ class GraphBuilder {
165
182
  }
166
183
  const visited = new Set([symbol]);
167
184
  const callerTree = yield this.expandCallers(graph.callers, depth - 1, visited);
168
- return { center: graph.center, callerTree, callees: graph.callees, importers };
185
+ return {
186
+ center: graph.center,
187
+ callerTree,
188
+ callees: graph.callees,
189
+ importers,
190
+ };
169
191
  });
170
192
  }
171
193
  expandCallers(callers, remainingDepth, visited) {
@@ -22,8 +22,10 @@ const TEST_FILE_RE = /\.(test|spec)\.[cm]?[jt]sx?$/i;
22
22
  const NATIVE_TEST_DIR_RE = /(^|\/)\w+Tests?(\/|$)/;
23
23
  const NATIVE_TEST_FILE_RE = /Tests?\.(swift|kt|java)$/;
24
24
  function isTestPath(filePath) {
25
- return TEST_DIR_RE.test(filePath) || TEST_FILE_RE.test(filePath)
26
- || NATIVE_TEST_DIR_RE.test(filePath) || NATIVE_TEST_FILE_RE.test(filePath);
25
+ return (TEST_DIR_RE.test(filePath) ||
26
+ TEST_FILE_RE.test(filePath) ||
27
+ NATIVE_TEST_DIR_RE.test(filePath) ||
28
+ NATIVE_TEST_FILE_RE.test(filePath));
27
29
  }
28
30
  const arrow_1 = require("../utils/arrow");
29
31
  /**
@@ -214,7 +216,7 @@ function findDependents(symbols_1, vectorDb_1, projectRoot_1, excludePaths_1) {
214
216
  const rows = yield table
215
217
  .query()
216
218
  .select(["path"])
217
- .where(`array_contains(referenced_symbols, '${(0, filter_builder_1.escapeSqlString)(sym)}') AND ${pathScope}`)
219
+ .where(`(array_contains(referenced_symbols, '${(0, filter_builder_1.escapeSqlString)(sym)}') OR array_contains(type_referenced_symbols, '${(0, filter_builder_1.escapeSqlString)(sym)}')) AND ${pathScope}`)
218
220
  .limit(200)
219
221
  .toArray();
220
222
  for (const row of rows) {