grepmax 0.21.2 → 0.22.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.
@@ -13,7 +13,21 @@ exports.GraphBuilder = void 0;
13
13
  const languages_1 = require("../core/languages");
14
14
  const filter_builder_1 = require("../utils/filter-builder");
15
15
  const query_timeout_1 = require("../utils/query-timeout");
16
+ const callsites_1 = require("./callsites");
16
17
  const graph_traversal_1 = require("./graph-traversal");
18
+ /** Sort key for the caller confidence tier: free call < member call < type ref. */
19
+ function edgeRank(node) {
20
+ switch (node.edgeKind) {
21
+ case "free":
22
+ return 0;
23
+ case "member":
24
+ return 1;
25
+ case "type":
26
+ return 2;
27
+ default:
28
+ return 3;
29
+ }
30
+ }
17
31
  class GraphBuilder {
18
32
  constructor(db, pathPrefix, excludePrefixes) {
19
33
  this.db = db;
@@ -58,6 +72,10 @@ class GraphBuilder {
58
72
  "start_line",
59
73
  "defined_symbols",
60
74
  "referenced_symbols",
75
+ // member_/type_ are needed so each caller edge can be tagged free vs
76
+ // member vs type (the confidence tier + builtin-member suppression).
77
+ "member_referenced_symbols",
78
+ "type_referenced_symbols",
61
79
  "role",
62
80
  "parent_symbol",
63
81
  "complexity",
@@ -75,7 +93,23 @@ class GraphBuilder {
75
93
  const fam = (0, languages_1.languageFamilyForPath)(String((_a = row.path) !== null && _a !== void 0 ? _a : ""));
76
94
  return fam == null || fam === anchorFamily;
77
95
  });
78
- return guarded.map((row) => this.mapRowToNode(row, symbol, "caller"));
96
+ const nodes = guarded.map((row) => this.mapRowToNode(row, symbol, "caller"));
97
+ // Caller-side builtin-member suppression. A member call `x.T()` to a builtin
98
+ // name T (`.get`/`.map`/`.forEach`) is almost always an unrelated stdlib
99
+ // method, not a real caller of the project symbol T — mirrors the callee-side
100
+ // guard in buildGraph. Only fires when T itself is a builtin name, so a normal
101
+ // symbol that merely has member callers is untouched.
102
+ const filtered = (0, callsites_1.isBuiltinCallee)(symbol)
103
+ ? nodes.filter((n) => n.edgeKind !== "member")
104
+ : nodes;
105
+ // Confidence sort: free calls (EXTRACTED) first, then member, then type,
106
+ // preserving scan order within a tier (Array.sort is stable). Keeps the
107
+ // trustworthy callers above the display caps (peek MAX_CALLERS, MCP limits)
108
+ // so guesses don't read as facts.
109
+ return filtered
110
+ .map((n, i) => ({ n, i }))
111
+ .sort((a, b) => edgeRank(a.n) - edgeRank(b.n) || a.i - b.i)
112
+ .map(({ n }) => n);
79
113
  });
80
114
  }
81
115
  /**
@@ -104,6 +138,7 @@ class GraphBuilder {
104
138
  */
105
139
  buildGraph(symbol) {
106
140
  return __awaiter(this, void 0, void 0, function* () {
141
+ var _a;
107
142
  const table = yield this.db.ensureTable();
108
143
  const escaped = (0, filter_builder_1.escapeSqlString)(symbol);
109
144
  // 1. Get Center (Definition)
@@ -126,13 +161,18 @@ class GraphBuilder {
126
161
  : null;
127
162
  // 2. Get Callers — anchored to the center definition's language family so a
128
163
  // bare name shared across languages doesn't pull in cross-language callers.
129
- const anchorFamily = center ? (0, languages_1.languageFamilyForPath)(center.file) : null;
130
- const callers = yield this.getCallers(symbol, anchorFamily);
164
+ const centerFamily = center ? (0, languages_1.languageFamilyForPath)(center.file) : null;
165
+ const callers = yield this.getCallers(symbol, centerFamily);
131
166
  // 3. Get Callees — resolve each to a GraphNode with file:line
132
167
  const calleeNames = center ? center.calls.slice(0, 15) : [];
168
+ const centerFile = center ? center.file : "";
133
169
  const calleeNodes = [];
134
170
  for (const name of calleeNames) {
135
171
  const esc = (0, filter_builder_1.escapeSqlString)(name);
172
+ // Pull a few candidates (was .limit(1)) so a callee name defined in more
173
+ // than one file can prefer the center's OWN file instead of an arbitrary
174
+ // same-named definition in an unrelated module — the cheapest correct
175
+ // disambiguation, and strictly better than the old first-row guess.
136
176
  const rows = yield table
137
177
  .query()
138
178
  .where(this.scopeWhere(`array_contains(defined_symbols, '${esc}')`))
@@ -145,12 +185,27 @@ class GraphBuilder {
145
185
  "parent_symbol",
146
186
  "complexity",
147
187
  ])
148
- .limit(1)
188
+ .limit(25)
149
189
  .toArray();
150
190
  if (rows.length > 0) {
151
- calleeNodes.push(this.mapRowToNode(rows[0], name, "center"));
191
+ // Prefer-self-file, then same-language-family. Only fall back to the
192
+ // first row when no candidate shares the center's file or family.
193
+ const selfRow = rows.find((r) => { var _a; return String((_a = r.path) !== null && _a !== void 0 ? _a : "") === centerFile; });
194
+ const familyRow = centerFamily
195
+ ? rows.find((r) => {
196
+ var _a;
197
+ return (0, languages_1.languageFamilyForPath)(String((_a = r.path) !== null && _a !== void 0 ? _a : "")) ===
198
+ centerFamily;
199
+ })
200
+ : undefined;
201
+ calleeNodes.push(this.mapRowToNode(((_a = selfRow !== null && selfRow !== void 0 ? selfRow : familyRow) !== null && _a !== void 0 ? _a : rows[0]), name, "center"));
152
202
  }
153
- else {
203
+ else if (!(0, callsites_1.isBuiltinCallee)(name)) {
204
+ // Unresolved + a known builtin (.map/.get/forEach) → suppress the
205
+ // phantom callee edge instead of emitting a "(not indexed)" stub.
206
+ // Mirrors the display-layer guard (peek.ts: `c.file || !isBuiltinCallee`):
207
+ // a project symbol that shadows a builtin name still resolves above and
208
+ // is kept, so only genuine builtins are dropped.
154
209
  calleeNodes.push({
155
210
  symbol: name,
156
211
  file: "",
@@ -240,10 +295,7 @@ class GraphBuilder {
240
295
  /** Distinct symbols that reference the given symbol (inbound edges). */
241
296
  callersOf(symbol) {
242
297
  return __awaiter(this, void 0, void 0, function* () {
243
- // Anchor the guard to this symbol's own definition language family so the
244
- // bare name doesn't pull in callers from an unrelated language.
245
- const loc = yield this.resolveLocation(symbol);
246
- const nodes = yield this.getCallers(symbol, loc ? (0, languages_1.languageFamilyForPath)(loc.file) : null);
298
+ const nodes = yield this.getAnchoredCallers(symbol);
247
299
  const out = [];
248
300
  for (const n of nodes) {
249
301
  if (n.symbol && n.symbol !== "unknown" && !out.includes(n.symbol)) {
@@ -274,6 +326,11 @@ class GraphBuilder {
274
326
  const out = [];
275
327
  for (const h of hits) {
276
328
  const loc = yield this.resolveLocation(h.symbol, anchorFamily);
329
+ // Drop unresolved builtins (.map/.get/forEach) reached via callee edges —
330
+ // same resolution-aware rule as buildGraph/peek. A neighbor that resolves
331
+ // to an indexed definition is kept even if it shadows a builtin name.
332
+ if (!loc && (0, callsites_1.isBuiltinCallee)(h.symbol))
333
+ continue;
277
334
  out.push(Object.assign(Object.assign({}, h), { file: (_a = loc === null || loc === void 0 ? void 0 : loc.file) !== null && _a !== void 0 ? _a : "", line: (_b = loc === null || loc === void 0 ? void 0 : loc.line) !== null && _b !== void 0 ? _b : 0 }));
278
335
  }
279
336
  return out;
@@ -292,12 +349,19 @@ class GraphBuilder {
292
349
  * several languages); falls back to the first definition when none match.
293
350
  */
294
351
  resolveLocation(symbol, anchorFamily) {
352
+ return __awaiter(this, void 0, void 0, function* () {
353
+ const def = yield this.resolveDefinition(symbol, anchorFamily);
354
+ return def ? { file: def.file, line: def.line } : null;
355
+ });
356
+ }
357
+ /** Resolve a symbol to its defining chunk plus language family metadata. */
358
+ resolveDefinition(symbol, anchorFamily) {
295
359
  return __awaiter(this, void 0, void 0, function* () {
296
360
  const table = yield this.db.ensureTable();
297
361
  const escaped = (0, filter_builder_1.escapeSqlString)(symbol);
298
362
  const rows = yield table
299
363
  .query()
300
- .select(["path", "start_line"])
364
+ .select(["path", "start_line", "is_exported"])
301
365
  .where(this.scopeWhere(`array_contains(defined_symbols, '${escaped}')`))
302
366
  // No anchor → keep the cheap single-row fetch. With one, pull a few
303
367
  // candidates so we can pick the same-family definition instead of guessing.
@@ -315,7 +379,20 @@ class GraphBuilder {
315
379
  if (match)
316
380
  r = match;
317
381
  }
318
- return { file: String(r.path || ""), line: Number(r.start_line || 0) };
382
+ const file = String(r.path || "");
383
+ return {
384
+ file,
385
+ line: Number(r.start_line || 0),
386
+ family: (0, languages_1.languageFamilyForPath)(file),
387
+ isExported: Boolean(r.is_exported),
388
+ };
389
+ });
390
+ }
391
+ getAnchoredCallers(symbol) {
392
+ return __awaiter(this, void 0, void 0, function* () {
393
+ var _a;
394
+ const def = yield this.resolveDefinition(symbol);
395
+ return this.getCallers(symbol, (_a = def === null || def === void 0 ? void 0 : def.family) !== null && _a !== void 0 ? _a : null);
319
396
  });
320
397
  }
321
398
  /**
@@ -366,6 +443,26 @@ class GraphBuilder {
366
443
  if (type === "center") {
367
444
  symbol = targetSymbol;
368
445
  }
446
+ // Classify HOW a caller references the target, for the confidence tier. Check
447
+ // member first: member names are ALSO in referenced_symbols (additive column),
448
+ // so a plain `referencedSymbols.includes` can't tell the two apart. Center and
449
+ // callee nodes get no edgeKind (not applicable).
450
+ let edgeKind;
451
+ let confidence;
452
+ if (type === "caller") {
453
+ if (toArray(row.member_referenced_symbols).includes(targetSymbol)) {
454
+ edgeKind = "member";
455
+ confidence = "INFERRED";
456
+ }
457
+ else if (referencedSymbols.includes(targetSymbol)) {
458
+ edgeKind = "free";
459
+ confidence = "EXTRACTED";
460
+ }
461
+ else if (toArray(row.type_referenced_symbols).includes(targetSymbol)) {
462
+ edgeKind = "type";
463
+ confidence = "INFERRED";
464
+ }
465
+ }
369
466
  return {
370
467
  symbol,
371
468
  file: row.path,
@@ -374,6 +471,8 @@ class GraphBuilder {
374
471
  calls: referencedSymbols,
375
472
  calledBy: [], // To be filled if we do reverse lookup
376
473
  complexity: row.complexity,
474
+ edgeKind,
475
+ confidence,
377
476
  };
378
477
  }
379
478
  }
@@ -13,6 +13,7 @@ exports.isTestPath = isTestPath;
13
13
  exports.resolveTargetSymbols = resolveTargetSymbols;
14
14
  exports.findTests = findTests;
15
15
  exports.findDependents = findDependents;
16
+ const languages_1 = require("../core/languages");
16
17
  const filter_builder_1 = require("../utils/filter-builder");
17
18
  const query_timeout_1 = require("../utils/query-timeout");
18
19
  const graph_builder_1 = require("./graph-builder");
@@ -50,11 +51,49 @@ function resolveTargetSymbols(target, vectorDb, projectRoot) {
50
51
  symbols.add(s);
51
52
  }
52
53
  }
53
- return { symbols: [...symbols], resolvedAsFile: true };
54
+ const family = (0, languages_1.languageFamilyForPath)(absPath);
55
+ return {
56
+ symbols: [...symbols],
57
+ resolvedAsFile: true,
58
+ symbolFamilies: new Map([...symbols].map((s) => [s, family])),
59
+ };
54
60
  }
55
61
  return { symbols: [target], resolvedAsFile: false };
56
62
  });
57
63
  }
64
+ function familyMatchesPath(anchorFamily, filePath) {
65
+ if (anchorFamily == null)
66
+ return true;
67
+ const family = (0, languages_1.languageFamilyForPath)(filePath);
68
+ return family == null || family === anchorFamily;
69
+ }
70
+ function resolveSymbolFamilies(symbols, vectorDb, projectRoot, excludePrefixes, seed) {
71
+ return __awaiter(this, void 0, void 0, function* () {
72
+ var _a;
73
+ const families = new Map(seed);
74
+ const missing = symbols.filter((s) => !families.has(s));
75
+ if (missing.length === 0)
76
+ return families;
77
+ const table = yield vectorDb.ensureTable();
78
+ const prefix = projectRoot.endsWith("/") ? projectRoot : `${projectRoot}/`;
79
+ let pathScope = (0, filter_builder_1.pathStartsWith)(prefix);
80
+ for (const ex of excludePrefixes !== null && excludePrefixes !== void 0 ? excludePrefixes : []) {
81
+ const exNorm = ex.endsWith("/") ? ex : `${ex}/`;
82
+ pathScope += ` AND ${(0, filter_builder_1.pathNotStartsWith)(exNorm)}`;
83
+ }
84
+ for (const sym of missing) {
85
+ const rows = yield table
86
+ .query()
87
+ .select(["path"])
88
+ .where(`array_contains(defined_symbols, '${(0, filter_builder_1.escapeSqlString)(sym)}') AND ${pathScope}`)
89
+ .limit(25)
90
+ .toArray();
91
+ const file = String(((_a = rows[0]) === null || _a === void 0 ? void 0 : _a.path) || "");
92
+ families.set(sym, file ? (0, languages_1.languageFamilyForPath)(file) : null);
93
+ }
94
+ return families;
95
+ });
96
+ }
58
97
  /**
59
98
  * For a single symbol, expand to include all symbols defined in the same file.
60
99
  * This catches cases where tests call methods of a class rather than the class name itself
@@ -109,16 +148,18 @@ function expandFileSymbols(symbols, vectorDb, projectRoot, excludePrefixes) {
109
148
  * Fallback hits are tagged with hops = -1.
110
149
  */
111
150
  function findTests(symbols_1, vectorDb_1, projectRoot_1) {
112
- return __awaiter(this, arguments, void 0, function* (symbols, vectorDb, projectRoot, depth = 1, excludePrefixes) {
151
+ return __awaiter(this, arguments, void 0, function* (symbols, vectorDb, projectRoot, depth = 1, excludePrefixes, symbolFamilies) {
152
+ var _a;
113
153
  const graphBuilder = new graph_builder_1.GraphBuilder(vectorDb, projectRoot, excludePrefixes);
114
154
  const testHits = new Map(); // key: file+symbol
115
155
  // Expand single-symbol targets to include all symbols from the same file
116
156
  const expanded = yield expandFileSymbols(symbols, vectorDb, projectRoot, excludePrefixes);
157
+ const families = yield resolveSymbolFamilies(expanded, vectorDb, projectRoot, excludePrefixes, symbolFamilies);
117
158
  for (const symbol of expanded) {
118
- yield walkCallers(symbol, graphBuilder, testHits, 0, depth, new Set());
159
+ yield walkCallers(symbol, graphBuilder, testHits, 0, depth, new Set(), (_a = families.get(symbol)) !== null && _a !== void 0 ? _a : null);
119
160
  }
120
161
  if (testHits.size === 0) {
121
- const importFiles = yield findImportFallbackTests(expanded, symbols, vectorDb, projectRoot, excludePrefixes);
162
+ const importFiles = yield findImportFallbackTests(expanded, symbols, vectorDb, projectRoot, excludePrefixes, families);
122
163
  for (const file of importFiles) {
123
164
  testHits.set(`${file}:(referenced)`, {
124
165
  file,
@@ -131,13 +172,14 @@ function findTests(symbols_1, vectorDb_1, projectRoot_1) {
131
172
  return [...testHits.values()].sort((a, b) => a.hops - b.hops || a.file.localeCompare(b.file));
132
173
  });
133
174
  }
134
- function findImportFallbackTests(expandedSymbols, originalSymbols, vectorDb, projectRoot, excludePrefixes) {
175
+ function findImportFallbackTests(expandedSymbols, originalSymbols, vectorDb, projectRoot, excludePrefixes, symbolFamilies) {
135
176
  return __awaiter(this, void 0, void 0, function* () {
177
+ var _a;
136
178
  const files = new Set();
137
179
  // Signal 1: referenced_symbols match (precise; works when the chunker
138
180
  // captured call references in test bodies). Uses the expanded set so tests
139
181
  // that call a method of the target class still match.
140
- const dependents = yield findDependents(expandedSymbols, vectorDb, projectRoot, undefined, 50, excludePrefixes);
182
+ const dependents = yield findDependents(expandedSymbols, vectorDb, projectRoot, undefined, 50, excludePrefixes, symbolFamilies);
141
183
  for (const d of dependents) {
142
184
  if (isTestPath(d.file))
143
185
  files.add(d.file);
@@ -155,6 +197,7 @@ function findImportFallbackTests(expandedSymbols, originalSymbols, vectorDb, pro
155
197
  // file symbols (helpers like `log`) textually drags in every test file
156
198
  // that mentions them, drowning the answer in false positives.
157
199
  for (const sym of originalSymbols) {
200
+ const family = (_a = symbolFamilies === null || symbolFamilies === void 0 ? void 0 : symbolFamilies.get(sym)) !== null && _a !== void 0 ? _a : null;
158
201
  // No .limit() here: LIKE + limit deadlocks in @lancedb 0.27.x when more
159
202
  // rows match than the limit (verified). Unlimited scan is fast; cap in JS.
160
203
  const rows = yield (0, query_timeout_1.withQueryTimeout)(table
@@ -164,19 +207,19 @@ function findImportFallbackTests(expandedSymbols, originalSymbols, vectorDb, pro
164
207
  .toArray(), `content LIKE %${sym}% (test fallback)`);
165
208
  for (const row of rows.slice(0, 500)) {
166
209
  const p = String(row.path || "");
167
- if (isTestPath(p))
210
+ if (isTestPath(p) && familyMatchesPath(family, p))
168
211
  files.add(p);
169
212
  }
170
213
  }
171
214
  return files;
172
215
  });
173
216
  }
174
- function walkCallers(symbol, graphBuilder, testHits, currentHop, maxDepth, visited) {
217
+ function walkCallers(symbol, graphBuilder, testHits, currentHop, maxDepth, visited, anchorFamily) {
175
218
  return __awaiter(this, void 0, void 0, function* () {
176
219
  if (visited.has(symbol))
177
220
  return;
178
221
  visited.add(symbol);
179
- const callers = yield graphBuilder.getCallers(symbol);
222
+ const callers = yield graphBuilder.getCallers(symbol, anchorFamily);
180
223
  for (const caller of callers) {
181
224
  if (isTestPath(caller.file)) {
182
225
  const key = `${caller.file}:${caller.symbol}`;
@@ -191,7 +234,7 @@ function walkCallers(symbol, graphBuilder, testHits, currentHop, maxDepth, visit
191
234
  }
192
235
  // Continue walking callers if within depth
193
236
  if (currentHop < maxDepth - 1) {
194
- yield walkCallers(caller.symbol, graphBuilder, testHits, currentHop + 1, maxDepth, visited);
237
+ yield walkCallers(caller.symbol, graphBuilder, testHits, currentHop + 1, maxDepth, visited, (0, languages_1.languageFamilyForPath)(caller.file));
195
238
  }
196
239
  }
197
240
  });
@@ -201,15 +244,18 @@ function walkCallers(symbol, graphBuilder, testHits, currentHop, maxDepth, visit
201
244
  * Returns files sorted by number of shared symbols (descending).
202
245
  */
203
246
  function findDependents(symbols_1, vectorDb_1, projectRoot_1, excludePaths_1) {
204
- return __awaiter(this, arguments, void 0, function* (symbols, vectorDb, projectRoot, excludePaths, limit = 10, excludePrefixes) {
247
+ return __awaiter(this, arguments, void 0, function* (symbols, vectorDb, projectRoot, excludePaths, limit = 10, excludePrefixes, symbolFamilies) {
248
+ var _a, _b;
205
249
  const table = yield vectorDb.ensureTable();
206
250
  let pathScope = (0, filter_builder_1.pathStartsWith)(`${projectRoot}/`);
207
251
  for (const ex of excludePrefixes !== null && excludePrefixes !== void 0 ? excludePrefixes : []) {
208
252
  const exNorm = ex.endsWith("/") ? ex : `${ex}/`;
209
253
  pathScope += ` AND ${(0, filter_builder_1.pathNotStartsWith)(exNorm)}`;
210
254
  }
211
- const counts = new Map();
255
+ const symbolsByFile = new Map();
256
+ const families = yield resolveSymbolFamilies(symbols, vectorDb, projectRoot, excludePrefixes, symbolFamilies);
212
257
  for (const sym of symbols) {
258
+ const family = (_a = families.get(sym)) !== null && _a !== void 0 ? _a : null;
213
259
  // 200, not 20: with per-chunk rows a popular symbol easily exceeds 20
214
260
  // chunks, and truncation here silently drops whole dependent files.
215
261
  // (array_contains + limit does not hit the LIKE+limit native hang.)
@@ -223,12 +269,16 @@ function findDependents(symbols_1, vectorDb_1, projectRoot_1, excludePaths_1) {
223
269
  const p = String(row.path || "");
224
270
  if (excludePaths === null || excludePaths === void 0 ? void 0 : excludePaths.has(p))
225
271
  continue;
226
- counts.set(p, (counts.get(p) || 0) + 1);
272
+ if (!familyMatchesPath(family, p))
273
+ continue;
274
+ const set = (_b = symbolsByFile.get(p)) !== null && _b !== void 0 ? _b : new Set();
275
+ set.add(sym);
276
+ symbolsByFile.set(p, set);
227
277
  }
228
278
  }
229
- return Array.from(counts.entries())
230
- .sort((a, b) => b[1] - a[1])
279
+ return Array.from(symbolsByFile.entries())
280
+ .sort((a, b) => b[1].size - a[1].size)
231
281
  .slice(0, limit)
232
- .map(([file, sharedSymbols]) => ({ file, sharedSymbols }));
282
+ .map(([file, symbols]) => ({ file, sharedSymbols: symbols.size }));
233
283
  });
234
284
  }
@@ -63,6 +63,7 @@ class ProjectBatchProcessor {
63
63
  this.pending = new Map();
64
64
  this.retryCount = new Map();
65
65
  this.debounceTimer = null;
66
+ this.activeBatch = null;
66
67
  this.processing = false;
67
68
  this.closed = false;
68
69
  this.currentBatchAc = null;
@@ -105,14 +106,34 @@ class ProjectBatchProcessor {
105
106
  var _a;
106
107
  this.closed = true;
107
108
  (_a = this.currentBatchAc) === null || _a === void 0 ? void 0 : _a.abort();
108
- if (this.debounceTimer)
109
+ if (this.debounceTimer) {
109
110
  clearTimeout(this.debounceTimer);
111
+ this.debounceTimer = null;
112
+ }
113
+ if (this.activeBatch) {
114
+ yield this.activeBatch;
115
+ }
110
116
  });
111
117
  }
112
118
  scheduleBatch() {
113
119
  if (this.debounceTimer)
114
120
  clearTimeout(this.debounceTimer);
115
- this.debounceTimer = setTimeout(() => this.processBatch(), DEBOUNCE_MS);
121
+ this.debounceTimer = setTimeout(() => {
122
+ this.debounceTimer = null;
123
+ this.startBatch();
124
+ }, DEBOUNCE_MS);
125
+ }
126
+ startBatch() {
127
+ if (this.activeBatch)
128
+ return;
129
+ const run = this.processBatch().catch((err) => {
130
+ console.error(`[${this.wtag}] Batch processing failed:`, err);
131
+ });
132
+ this.activeBatch = run;
133
+ void run.finally(() => {
134
+ if (this.activeBatch === run)
135
+ this.activeBatch = null;
136
+ });
116
137
  }
117
138
  processBatch() {
118
139
  return __awaiter(this, void 0, void 0, function* () {
@@ -124,7 +145,10 @@ class ProjectBatchProcessor {
124
145
  (0, logger_1.log)(this.wtag, "Disk critically low — deferring batch processing");
125
146
  if (this.debounceTimer)
126
147
  clearTimeout(this.debounceTimer);
127
- this.debounceTimer = setTimeout(() => this.processBatch(), 60000);
148
+ this.debounceTimer = setTimeout(() => {
149
+ this.debounceTimer = null;
150
+ this.startBatch();
151
+ }, 60000);
128
152
  return;
129
153
  }
130
154
  this.processing = true;
@@ -158,11 +182,10 @@ class ProjectBatchProcessor {
158
182
  const vectors = [];
159
183
  const metaUpdates = new Map();
160
184
  const metaDeletes = [];
161
- const attempted = new Set();
185
+ const completed = new Set();
162
186
  for (const [absPath, event] of batch) {
163
187
  if (batchAc.signal.aborted)
164
188
  break;
165
- attempted.add(absPath);
166
189
  processed++;
167
190
  if (batch.size > 10 &&
168
191
  (processed % 10 === 0 || processed === batch.size)) {
@@ -172,6 +195,7 @@ class ProjectBatchProcessor {
172
195
  deletes.push(absPath);
173
196
  metaDeletes.push(absPath);
174
197
  reindexed++;
198
+ completed.add(absPath);
175
199
  continue;
176
200
  }
177
201
  // change or add
@@ -186,10 +210,12 @@ class ProjectBatchProcessor {
186
210
  metaDeletes.push(absPath);
187
211
  reindexed++;
188
212
  }
213
+ completed.add(absPath);
189
214
  continue;
190
215
  }
191
216
  const cached = this.metaCache.get(absPath);
192
217
  if ((0, cache_check_1.isFileCached)(cached, stats)) {
218
+ completed.add(absPath);
193
219
  continue;
194
220
  }
195
221
  // Fast path: if only mtime changed but size matches and we have a hash,
@@ -199,6 +225,7 @@ class ProjectBatchProcessor {
199
225
  const hash = (0, file_utils_1.computeContentHash)(buf, absPath);
200
226
  if (hash === cached.hash) {
201
227
  metaUpdates.set(absPath, Object.assign(Object.assign({}, cached), { mtimeMs: stats.mtimeMs }));
228
+ completed.add(absPath);
202
229
  continue;
203
230
  }
204
231
  }
@@ -213,12 +240,14 @@ class ProjectBatchProcessor {
213
240
  };
214
241
  if (cached && cached.hash === result.hash) {
215
242
  metaUpdates.set(absPath, metaEntry);
243
+ completed.add(absPath);
216
244
  continue;
217
245
  }
218
246
  if (result.shouldDelete) {
219
247
  deletes.push(absPath);
220
248
  metaUpdates.set(absPath, metaEntry);
221
249
  reindexed++;
250
+ completed.add(absPath);
222
251
  continue;
223
252
  }
224
253
  deletes.push(absPath);
@@ -227,6 +256,7 @@ class ProjectBatchProcessor {
227
256
  }
228
257
  metaUpdates.set(absPath, metaEntry);
229
258
  reindexed++;
259
+ completed.add(absPath);
230
260
  }
231
261
  catch (err) {
232
262
  if (batchAc.signal.aborted)
@@ -236,6 +266,7 @@ class ProjectBatchProcessor {
236
266
  deletes.push(absPath);
237
267
  metaDeletes.push(absPath);
238
268
  reindexed++;
269
+ completed.add(absPath);
239
270
  }
240
271
  else {
241
272
  console.error(`[${this.wtag}] Failed to process ${absPath}:`, err);
@@ -243,12 +274,14 @@ class ProjectBatchProcessor {
243
274
  console.error(`[${this.wtag}] Worker pool unhealthy, aborting batch`);
244
275
  break;
245
276
  }
277
+ completed.add(absPath);
246
278
  }
247
279
  }
248
280
  }
249
- // Requeue files that weren't attempted (aborted or pool unhealthy)
281
+ // Requeue files that didn't reach a terminal outcome. This includes the
282
+ // file whose worker was in flight when the batch was aborted.
250
283
  for (const [absPath, event] of batch) {
251
- if (!attempted.has(absPath) && !this.pending.has(absPath)) {
284
+ if (!completed.has(absPath) && !this.pending.has(absPath)) {
252
285
  this.pending.set(absPath, event);
253
286
  }
254
287
  }
@@ -339,11 +372,14 @@ class ProjectBatchProcessor {
339
372
  clearTimeout(batchTimeout);
340
373
  this.currentBatchAc = null;
341
374
  this.processing = false;
342
- if (this.pending.size > 0) {
375
+ if (!this.closed && this.pending.size > 0) {
343
376
  if (backoffOverrideMs > 0) {
344
377
  if (this.debounceTimer)
345
378
  clearTimeout(this.debounceTimer);
346
- this.debounceTimer = setTimeout(() => this.processBatch(), backoffOverrideMs);
379
+ this.debounceTimer = setTimeout(() => {
380
+ this.debounceTimer = null;
381
+ this.startBatch();
382
+ }, backoffOverrideMs);
347
383
  }
348
384
  else {
349
385
  this.scheduleBatch();
@@ -571,6 +571,15 @@ class TreeSitterChunker {
571
571
  typeReferencedSymbols.push(n);
572
572
  }
573
573
  };
574
+ // Member-call names (`obj.method()`), recorded additively alongside
575
+ // referencedSymbols (see Chunk.memberReferencedSymbols). Deduped, like
576
+ // type refs — a future resolver queries membership, not occurrence count.
577
+ const memberReferencedSymbols = [];
578
+ const addMemberRef = (n) => {
579
+ if (n && !memberReferencedSymbols.includes(n)) {
580
+ memberReferencedSymbols.push(n);
581
+ }
582
+ };
574
583
  // Leaf identifier node types across grammars (a bare name with no
575
584
  // named children — `ErrorCodes`, not `a.ErrorCodes`).
576
585
  const LEAF_ID_TYPES = new Set([
@@ -677,24 +686,33 @@ class TreeSitterChunker {
677
686
  : null;
678
687
  if (func) {
679
688
  let funcName = func.text;
689
+ let isMemberCall = false;
680
690
  // Handle member access (obj.method) to extract just 'method'
681
691
  if (func.type === "member_expression") {
682
692
  // JS/TS: object.property
683
693
  const prop = func.childForFieldName
684
694
  ? func.childForFieldName("property")
685
695
  : null;
686
- if (prop)
696
+ if (prop) {
687
697
  funcName = prop.text;
698
+ isMemberCall = true;
699
+ }
688
700
  }
689
701
  else if (func.type === "attribute") {
690
702
  // Python: object.attribute
691
703
  const attr = func.childForFieldName
692
704
  ? func.childForFieldName("attribute")
693
705
  : null;
694
- if (attr)
706
+ if (attr) {
695
707
  funcName = attr.text;
708
+ isMemberCall = true;
709
+ }
696
710
  }
697
711
  referencedSymbols.push(funcName);
712
+ // Additive: member calls ALSO go here. referencedSymbols is left
713
+ // untouched (no recall loss); this just tags which were members.
714
+ if (isMemberCall)
715
+ addMemberRef(funcName);
698
716
  }
699
717
  else {
700
718
  // Swift/Kotlin: call_expression has no "function" field;
@@ -702,15 +720,20 @@ class TreeSitterChunker {
702
720
  const firstChild = ((_a = n.namedChildren) !== null && _a !== void 0 ? _a : [])[0];
703
721
  if (firstChild) {
704
722
  let funcName = firstChild.text;
723
+ let isMemberCall = false;
705
724
  if (firstChild.type === "navigation_expression") {
706
725
  const suffix = ((_b = firstChild.namedChildren) !== null && _b !== void 0 ? _b : []).find((c) => c.type === "navigation_suffix");
707
726
  const methodId = suffix
708
727
  ? ((_c = suffix.namedChildren) !== null && _c !== void 0 ? _c : []).find((c) => c.type === "simple_identifier")
709
728
  : null;
710
- if (methodId)
729
+ if (methodId) {
711
730
  funcName = methodId.text;
731
+ isMemberCall = true;
732
+ }
712
733
  }
713
734
  referencedSymbols.push(funcName);
735
+ if (isMemberCall)
736
+ addMemberRef(funcName);
714
737
  }
715
738
  }
716
739
  }
@@ -905,6 +928,7 @@ class TreeSitterChunker {
905
928
  definedSymbols,
906
929
  referencedSymbols,
907
930
  typeReferencedSymbols,
931
+ memberReferencedSymbols,
908
932
  role,
909
933
  parentSymbol: stack.length > 1
910
934
  ? stack[stack.length - 1].replace(/^(Class|Method|Function|Interface|Type): /, "")
@@ -990,6 +1014,9 @@ class TreeSitterChunker {
990
1014
  if (sub.typeReferencedSymbols) {
991
1015
  sub.typeReferencedSymbols = sub.typeReferencedSymbols.filter(occurs);
992
1016
  }
1017
+ if (sub.memberReferencedSymbols) {
1018
+ sub.memberReferencedSymbols = sub.memberReferencedSymbols.filter(occurs);
1019
+ }
993
1020
  return sub;
994
1021
  }
995
1022
  splitIfTooBig(chunk) {