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.
- package/.claude-plugin/marketplace.json +2 -4
- package/dist/commands/dead.js +2 -1
- package/dist/commands/impact.js +3 -3
- package/dist/commands/mcp.js +14 -7
- package/dist/commands/peek.js +17 -4
- package/dist/commands/plugin.js +34 -24
- package/dist/commands/search-run.js +2 -0
- package/dist/commands/search.js +21 -11
- package/dist/commands/test-find.js +2 -2
- package/dist/commands/trace.js +13 -5
- package/dist/config.js +6 -1
- package/dist/lib/daemon/ipc-handler.js +1 -0
- package/dist/lib/daemon/mlx-server-manager.js +52 -8
- package/dist/lib/daemon/process-manager.js +16 -17
- package/dist/lib/graph/graph-builder.js +111 -12
- package/dist/lib/graph/impact.js +66 -16
- package/dist/lib/index/batch-processor.js +45 -9
- package/dist/lib/index/chunker.js +30 -3
- package/dist/lib/llm/tools.js +3 -3
- package/dist/lib/search/searcher.js +26 -21
- package/dist/lib/store/vector-db.js +52 -23
- package/dist/lib/utils/daemon-client.js +35 -9
- package/dist/lib/workers/orchestrator.js +1 -0
- package/dist/lib/workers/pool.js +62 -20
- package/package.json +8 -3
- package/plugins/grepmax/.claude-plugin/plugin.json +2 -8
- package/scripts/postinstall.js +8 -115
|
@@ -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
|
-
|
|
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
|
|
130
|
-
const callers = yield this.getCallers(symbol,
|
|
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(
|
|
188
|
+
.limit(25)
|
|
149
189
|
.toArray();
|
|
150
190
|
if (rows.length > 0) {
|
|
151
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|
package/dist/lib/graph/impact.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
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(
|
|
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,
|
|
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(() =>
|
|
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(() =>
|
|
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
|
|
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
|
|
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 (!
|
|
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(() =>
|
|
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) {
|