grepmax 0.21.3 → 0.23.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/README.md +2 -1
- package/dist/commands/audit.js +111 -5
- package/dist/commands/dead.js +2 -1
- package/dist/commands/impact.js +3 -3
- package/dist/commands/mcp.js +29 -9
- 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/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 +92 -13
- package/dist/lib/graph/impact.js +66 -16
- package/dist/lib/index/batch-processor.js +45 -9
- package/dist/lib/llm/tools.js +3 -3
- package/dist/lib/search/searcher.js +26 -21
- package/dist/lib/store/vector-db.js +20 -6
- package/dist/lib/utils/daemon-client.js +35 -9
- package/dist/lib/workers/pool.js +62 -20
- package/package.json +9 -4
- package/plugins/grepmax/.claude-plugin/plugin.json +2 -8
- package/scripts/postinstall.js +8 -115
package/dist/commands/trace.js
CHANGED
|
@@ -56,6 +56,10 @@ const stale_hint_1 = require("../lib/utils/stale-hint");
|
|
|
56
56
|
const useColors = process.stdout.isTTY && !process.env.NO_COLOR;
|
|
57
57
|
const dim = (s) => (useColors ? `\x1b[2m${s}\x1b[22m` : s);
|
|
58
58
|
const bold = (s) => (useColors ? `\x1b[1m${s}\x1b[22m` : s);
|
|
59
|
+
// Inferred-edge marker for caller rows: `member` = receiver-unverified `x.T()`,
|
|
60
|
+
// `type` = a type-position reference (not a call). Free calls stay unmarked —
|
|
61
|
+
// the trustworthy default — so only guesses carry a tag.
|
|
62
|
+
const kindTag = (k) => k === "member" || k === "type" ? ` (${k})` : "";
|
|
59
63
|
function formatTraceAgent(graph, projectRoot, raw = false) {
|
|
60
64
|
if (!graph.center)
|
|
61
65
|
return "(not found)";
|
|
@@ -71,7 +75,7 @@ function formatTraceAgent(graph, projectRoot, raw = false) {
|
|
|
71
75
|
function walkCallers(tree, depth) {
|
|
72
76
|
if (raw) {
|
|
73
77
|
for (const t of tree) {
|
|
74
|
-
lines.push(`${" ".repeat(depth)}<- ${t.node.symbol}\t${rel(t.node.file)}:${t.node.line}`);
|
|
78
|
+
lines.push(`${" ".repeat(depth)}<- ${t.node.symbol}\t${rel(t.node.file)}:${t.node.line}${kindTag(t.node.edgeKind)}`);
|
|
75
79
|
walkCallers(t.callers, depth + 1);
|
|
76
80
|
}
|
|
77
81
|
return;
|
|
@@ -92,6 +96,9 @@ function formatTraceAgent(graph, projectRoot, raw = false) {
|
|
|
92
96
|
file: t.node.file,
|
|
93
97
|
lineSet: new Set(),
|
|
94
98
|
sub: [],
|
|
99
|
+
// Callers arrive free-first (getCallers sorts), so the first edgeKind
|
|
100
|
+
// seen for this group is its highest-confidence one.
|
|
101
|
+
edgeKind: t.node.edgeKind,
|
|
95
102
|
};
|
|
96
103
|
groups.set(key, g);
|
|
97
104
|
order.push(key);
|
|
@@ -103,7 +110,7 @@ function formatTraceAgent(graph, projectRoot, raw = false) {
|
|
|
103
110
|
const g = groups.get(key);
|
|
104
111
|
const locs = [...g.lineSet].sort((a, b) => a - b).join(",");
|
|
105
112
|
const loc = g.file ? `${rel(g.file)}:${locs}` : "(not indexed)";
|
|
106
|
-
lines.push(`${" ".repeat(depth)}<- ${g.symbol}\t${loc}`);
|
|
113
|
+
lines.push(`${" ".repeat(depth)}<- ${g.symbol}\t${loc}${kindTag(g.edgeKind)}`);
|
|
107
114
|
walkCallers(g.sub, depth + 1);
|
|
108
115
|
}
|
|
109
116
|
}
|
|
@@ -139,6 +146,7 @@ function buildInboundTree(callerTree, targetSymbol, fileCache, withSnippets, lim
|
|
|
139
146
|
line: t.node.line,
|
|
140
147
|
snippet: (_b = snippet === null || snippet === void 0 ? void 0 : snippet.snippet) !== null && _b !== void 0 ? _b : null,
|
|
141
148
|
snippetLine: (_c = snippet === null || snippet === void 0 ? void 0 : snippet.snippetLine) !== null && _c !== void 0 ? _c : null,
|
|
149
|
+
edgeKind: t.node.edgeKind,
|
|
142
150
|
callers: buildInboundTree(t.callers, t.node.symbol, fileCache, withSnippets, limit),
|
|
143
151
|
});
|
|
144
152
|
if (out.length >= limit)
|
|
@@ -158,8 +166,8 @@ function formatInboundAgent(center, tree, projectRoot, withSnippets) {
|
|
|
158
166
|
? `${rel(n.file)}:${((_a = n.snippetLine) !== null && _a !== void 0 ? _a : n.line) + 1}`
|
|
159
167
|
: "(not indexed)";
|
|
160
168
|
const cols = withSnippets
|
|
161
|
-
? `${loc}\t${n.symbol}\t${(_b = n.snippet) !== null && _b !== void 0 ? _b : ""}`
|
|
162
|
-
: `${loc}\t${n.symbol}`;
|
|
169
|
+
? `${loc}\t${n.symbol}${kindTag(n.edgeKind)}\t${(_b = n.snippet) !== null && _b !== void 0 ? _b : ""}`
|
|
170
|
+
: `${loc}\t${n.symbol}${kindTag(n.edgeKind)}`;
|
|
163
171
|
lines.push(`${prefix}${cols}`);
|
|
164
172
|
walk(n.callers, depth + 1);
|
|
165
173
|
}
|
|
@@ -194,7 +202,7 @@ function formatInboundHuman(center, tree, projectRoot, withSnippets) {
|
|
|
194
202
|
const loc = n.file
|
|
195
203
|
? `${rel(n.file)}:${((_a = n.snippetLine) !== null && _a !== void 0 ? _a : n.line) + 1}`
|
|
196
204
|
: "(not indexed)";
|
|
197
|
-
lines.push(`${indent}${n.symbol} ${dim(loc)}`);
|
|
205
|
+
lines.push(`${indent}${n.symbol} ${dim(loc)}${dim(kindTag(n.edgeKind))}`);
|
|
198
206
|
if (withSnippets && n.snippet) {
|
|
199
207
|
lines.push(`${indent} ${dim(n.snippet)}`);
|
|
200
208
|
}
|
|
@@ -147,22 +147,66 @@ class MlxServerManager {
|
|
|
147
147
|
const env = Object.assign({}, process.env);
|
|
148
148
|
if (mlxModel)
|
|
149
149
|
env.MLX_EMBED_MODEL = mlxModel;
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
150
|
+
const closeLogFd = () => {
|
|
151
|
+
try {
|
|
152
|
+
fs.closeSync(logFd);
|
|
153
|
+
}
|
|
154
|
+
catch (_a) { }
|
|
155
|
+
};
|
|
156
|
+
try {
|
|
157
|
+
this.mlxChild = (0, node_child_process_1.spawn)("uv", ["run", "python", "server.py"], {
|
|
158
|
+
cwd: serverDir,
|
|
159
|
+
detached: true,
|
|
160
|
+
stdio: ["ignore", logFd, logFd],
|
|
161
|
+
env,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
catch (err) {
|
|
165
|
+
closeLogFd();
|
|
166
|
+
console.error(`[daemon] MLX embed server failed to spawn: ${err instanceof Error ? err.message : String(err)} — falling back to CPU embeddings`);
|
|
167
|
+
this.mlxChild = null;
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
const child = this.mlxChild;
|
|
171
|
+
let startupSettled = false;
|
|
172
|
+
let resolveStartupError;
|
|
173
|
+
const startupError = new Promise((resolve) => {
|
|
174
|
+
resolveStartupError = resolve;
|
|
155
175
|
});
|
|
156
|
-
|
|
157
|
-
|
|
176
|
+
const onChildError = (err) => {
|
|
177
|
+
if (!startupSettled) {
|
|
178
|
+
resolveStartupError(err);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
console.error(`[daemon] MLX embed server process error: ${err.message}`);
|
|
182
|
+
if (this.mlxChild === child)
|
|
183
|
+
this.mlxChild = null;
|
|
184
|
+
};
|
|
185
|
+
child.on("error", onChildError);
|
|
186
|
+
child.unref();
|
|
187
|
+
console.log(`[daemon] Starting MLX embed server (PID: ${child.pid})`);
|
|
158
188
|
// Poll for readiness (up to 30s)
|
|
159
189
|
for (let i = 0; i < 30; i++) {
|
|
160
|
-
|
|
190
|
+
const spawnError = yield Promise.race([
|
|
191
|
+
startupError,
|
|
192
|
+
new Promise((r) => setTimeout(() => r(null), 1000)),
|
|
193
|
+
]);
|
|
194
|
+
if (spawnError) {
|
|
195
|
+
startupSettled = true;
|
|
196
|
+
child.off("error", onChildError);
|
|
197
|
+
closeLogFd();
|
|
198
|
+
console.error(`[daemon] MLX embed server failed to spawn: ${spawnError.message} — falling back to CPU embeddings`);
|
|
199
|
+
if (this.mlxChild === child)
|
|
200
|
+
this.mlxChild = null;
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
161
203
|
if (yield this.isMlxServerUp()) {
|
|
204
|
+
startupSettled = true;
|
|
162
205
|
console.log("[daemon] MLX embed server ready");
|
|
163
206
|
return;
|
|
164
207
|
}
|
|
165
208
|
}
|
|
209
|
+
startupSettled = true;
|
|
166
210
|
console.error("[daemon] MLX embed server failed to start within 30s — falling back to CPU embeddings");
|
|
167
211
|
this.mlxChild = null;
|
|
168
212
|
});
|
|
@@ -84,7 +84,7 @@ class ProcessManager {
|
|
|
84
84
|
return __awaiter(this, void 0, void 0, function* () {
|
|
85
85
|
// 1. Check for other daemon processes
|
|
86
86
|
const daemonPids = this.findProcessesByTitle("gmax-daemon").filter((pid) => pid !== process.pid);
|
|
87
|
-
|
|
87
|
+
let workerPids = this.findProcessesByTitle("gmax-worker");
|
|
88
88
|
if (daemonPids.length === 0 && workerPids.length === 0) {
|
|
89
89
|
(0, logger_1.log)("daemon", "No stale processes found");
|
|
90
90
|
return;
|
|
@@ -92,15 +92,21 @@ class ProcessManager {
|
|
|
92
92
|
// A peer that's gracefully shutting down has already dropped its
|
|
93
93
|
// socket/PID/lock (so the liveness probes below read "stale") yet is still
|
|
94
94
|
// mid-cleanup — destroying workers, closing LanceDB. Killing it now corrupts
|
|
95
|
-
// that teardown. Defer to its draining marker
|
|
96
|
-
//
|
|
97
|
-
let anyDraining = false;
|
|
95
|
+
// that teardown. Defer to its draining marker until the process exits; only
|
|
96
|
+
// after the marker grace elapses do we treat it like any other stale daemon.
|
|
98
97
|
for (const pid of daemonPids) {
|
|
99
98
|
(0, logger_1.log)("daemon", `found daemon PID:${pid}, checking liveness...`);
|
|
100
99
|
if ((0, daemon_client_1.isDaemonDraining)(pid)) {
|
|
101
|
-
|
|
102
|
-
(0,
|
|
103
|
-
|
|
100
|
+
(0, logger_1.log)("daemon", `daemon PID:${pid} is gracefully draining — waiting for it to exit`);
|
|
101
|
+
const exited = yield (0, daemon_client_1.waitForDaemonDrain)(pid);
|
|
102
|
+
if (exited) {
|
|
103
|
+
(0, logger_1.log)("daemon", `daemon PID:${pid} finished draining`);
|
|
104
|
+
// The peer may have reaped its workers while we waited. Rescan before
|
|
105
|
+
// sweeping so we don't act on a stale process snapshot.
|
|
106
|
+
workerPids = this.findProcessesByTitle("gmax-worker");
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
(0, logger_1.log)("daemon", `daemon PID:${pid} still exists after drain grace — checking stale probes`);
|
|
104
110
|
}
|
|
105
111
|
// A busy daemon (mid-index, compaction, big LMDB write) can block the
|
|
106
112
|
// event loop long enough to miss a ping. Two independent liveness
|
|
@@ -120,16 +126,9 @@ class ProcessManager {
|
|
|
120
126
|
}
|
|
121
127
|
// 2. Kill orphaned workers from previous daemon instances.
|
|
122
128
|
// Safe because this runs before the new daemon's worker pool is initialized.
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
(0, logger_1.log)("daemon", "skipping orphan-worker sweep — a peer is draining its own workers");
|
|
127
|
-
}
|
|
128
|
-
else {
|
|
129
|
-
for (const pid of workerPids) {
|
|
130
|
-
(0, logger_1.log)("daemon", `killing orphaned worker PID:${pid}`);
|
|
131
|
-
yield (0, process_1.killProcess)(pid);
|
|
132
|
-
}
|
|
129
|
+
for (const pid of workerPids) {
|
|
130
|
+
(0, logger_1.log)("daemon", `killing orphaned worker PID:${pid}`);
|
|
131
|
+
yield (0, process_1.killProcess)(pid);
|
|
133
132
|
}
|
|
134
133
|
(0, logger_1.log)("daemon", `Cleaned up ${daemonPids.length} stale daemon(s), ${workerPids.length} orphaned worker(s)`);
|
|
135
134
|
});
|
|
@@ -15,6 +15,19 @@ const filter_builder_1 = require("../utils/filter-builder");
|
|
|
15
15
|
const query_timeout_1 = require("../utils/query-timeout");
|
|
16
16
|
const callsites_1 = require("./callsites");
|
|
17
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
|
+
}
|
|
18
31
|
class GraphBuilder {
|
|
19
32
|
constructor(db, pathPrefix, excludePrefixes) {
|
|
20
33
|
this.db = db;
|
|
@@ -59,6 +72,10 @@ class GraphBuilder {
|
|
|
59
72
|
"start_line",
|
|
60
73
|
"defined_symbols",
|
|
61
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",
|
|
62
79
|
"role",
|
|
63
80
|
"parent_symbol",
|
|
64
81
|
"complexity",
|
|
@@ -76,7 +93,23 @@ class GraphBuilder {
|
|
|
76
93
|
const fam = (0, languages_1.languageFamilyForPath)(String((_a = row.path) !== null && _a !== void 0 ? _a : ""));
|
|
77
94
|
return fam == null || fam === anchorFamily;
|
|
78
95
|
});
|
|
79
|
-
|
|
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);
|
|
80
113
|
});
|
|
81
114
|
}
|
|
82
115
|
/**
|
|
@@ -105,6 +138,7 @@ class GraphBuilder {
|
|
|
105
138
|
*/
|
|
106
139
|
buildGraph(symbol) {
|
|
107
140
|
return __awaiter(this, void 0, void 0, function* () {
|
|
141
|
+
var _a;
|
|
108
142
|
const table = yield this.db.ensureTable();
|
|
109
143
|
const escaped = (0, filter_builder_1.escapeSqlString)(symbol);
|
|
110
144
|
// 1. Get Center (Definition)
|
|
@@ -127,8 +161,8 @@ class GraphBuilder {
|
|
|
127
161
|
: null;
|
|
128
162
|
// 2. Get Callers — anchored to the center definition's language family so a
|
|
129
163
|
// bare name shared across languages doesn't pull in cross-language callers.
|
|
130
|
-
const
|
|
131
|
-
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);
|
|
132
166
|
// 3. Get Callees — resolve each to a GraphNode with file:line
|
|
133
167
|
const calleeNames = center ? center.calls.slice(0, 15) : [];
|
|
134
168
|
const centerFile = center ? center.file : "";
|
|
@@ -154,11 +188,17 @@ class GraphBuilder {
|
|
|
154
188
|
.limit(25)
|
|
155
189
|
.toArray();
|
|
156
190
|
if (rows.length > 0) {
|
|
157
|
-
// Prefer-self-file
|
|
158
|
-
//
|
|
159
|
-
// candidate is in the center's file (unchanged behaviour then).
|
|
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.
|
|
160
193
|
const selfRow = rows.find((r) => { var _a; return String((_a = r.path) !== null && _a !== void 0 ? _a : "") === centerFile; });
|
|
161
|
-
|
|
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"));
|
|
162
202
|
}
|
|
163
203
|
else if (!(0, callsites_1.isBuiltinCallee)(name)) {
|
|
164
204
|
// Unresolved + a known builtin (.map/.get/forEach) → suppress the
|
|
@@ -255,10 +295,7 @@ class GraphBuilder {
|
|
|
255
295
|
/** Distinct symbols that reference the given symbol (inbound edges). */
|
|
256
296
|
callersOf(symbol) {
|
|
257
297
|
return __awaiter(this, void 0, void 0, function* () {
|
|
258
|
-
|
|
259
|
-
// bare name doesn't pull in callers from an unrelated language.
|
|
260
|
-
const loc = yield this.resolveLocation(symbol);
|
|
261
|
-
const nodes = yield this.getCallers(symbol, loc ? (0, languages_1.languageFamilyForPath)(loc.file) : null);
|
|
298
|
+
const nodes = yield this.getAnchoredCallers(symbol);
|
|
262
299
|
const out = [];
|
|
263
300
|
for (const n of nodes) {
|
|
264
301
|
if (n.symbol && n.symbol !== "unknown" && !out.includes(n.symbol)) {
|
|
@@ -312,12 +349,19 @@ class GraphBuilder {
|
|
|
312
349
|
* several languages); falls back to the first definition when none match.
|
|
313
350
|
*/
|
|
314
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) {
|
|
315
359
|
return __awaiter(this, void 0, void 0, function* () {
|
|
316
360
|
const table = yield this.db.ensureTable();
|
|
317
361
|
const escaped = (0, filter_builder_1.escapeSqlString)(symbol);
|
|
318
362
|
const rows = yield table
|
|
319
363
|
.query()
|
|
320
|
-
.select(["path", "start_line"])
|
|
364
|
+
.select(["path", "start_line", "is_exported"])
|
|
321
365
|
.where(this.scopeWhere(`array_contains(defined_symbols, '${escaped}')`))
|
|
322
366
|
// No anchor → keep the cheap single-row fetch. With one, pull a few
|
|
323
367
|
// candidates so we can pick the same-family definition instead of guessing.
|
|
@@ -335,7 +379,20 @@ class GraphBuilder {
|
|
|
335
379
|
if (match)
|
|
336
380
|
r = match;
|
|
337
381
|
}
|
|
338
|
-
|
|
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);
|
|
339
396
|
});
|
|
340
397
|
}
|
|
341
398
|
/**
|
|
@@ -386,6 +443,26 @@ class GraphBuilder {
|
|
|
386
443
|
if (type === "center") {
|
|
387
444
|
symbol = targetSymbol;
|
|
388
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
|
+
}
|
|
389
466
|
return {
|
|
390
467
|
symbol,
|
|
391
468
|
file: row.path,
|
|
@@ -394,6 +471,8 @@ class GraphBuilder {
|
|
|
394
471
|
calls: referencedSymbols,
|
|
395
472
|
calledBy: [], // To be filled if we do reverse lookup
|
|
396
473
|
complexity: row.complexity,
|
|
474
|
+
edgeKind,
|
|
475
|
+
confidence,
|
|
397
476
|
};
|
|
398
477
|
}
|
|
399
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
|
}
|