grepmax 0.26.26 → 0.26.28
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 +1 -1
- package/README.md +40 -0
- package/dist/commands/audit.js +28 -260
- package/dist/commands/context.js +42 -26
- package/dist/commands/dead.js +37 -35
- package/dist/commands/doctor.js +280 -10
- package/dist/commands/extract.js +79 -49
- package/dist/commands/impact.js +74 -20
- package/dist/commands/log.js +25 -25
- package/dist/commands/mcp.js +622 -474
- package/dist/commands/peek.js +49 -47
- package/dist/commands/project.js +29 -107
- package/dist/commands/related.js +43 -52
- package/dist/commands/search-run.js +23 -8
- package/dist/commands/search.js +7 -2
- package/dist/commands/similar.js +50 -76
- package/dist/commands/skeleton.js +137 -83
- package/dist/commands/status.js +90 -25
- package/dist/commands/surprises.js +73 -16
- package/dist/commands/symbols.js +58 -77
- package/dist/commands/test-find.js +51 -17
- package/dist/commands/trace.js +26 -11
- package/dist/lib/daemon/daemon.js +26 -0
- package/dist/lib/daemon/graph-handler.js +853 -0
- package/dist/lib/daemon/ipc-handler.js +19 -2
- package/dist/lib/daemon/read-verbs.js +105 -0
- package/dist/lib/daemon/rows-handler.js +665 -0
- package/dist/lib/daemon/vector-handler.js +199 -0
- package/dist/lib/store/vector-db.js +1 -1
- package/dist/lib/utils/daemon-client.js +11 -0
- package/dist/lib/utils/store-access.js +381 -0
- package/dist/lib/utils/tests-footer.js +19 -2
- package/dist/lib/utils/watcher-launcher.js +10 -0
- package/package.json +2 -2
- package/plugins/grepmax/.claude-plugin/plugin.json +1 -1
- package/scripts/postinstall.js +0 -69
|
@@ -0,0 +1,665 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Daemon-side handlers for the `rows.*` read verbs.
|
|
4
|
+
*
|
|
5
|
+
* These serve the commands whose store work is a plain row select: `symbols`,
|
|
6
|
+
* `project`, `extract`, `related`, `log`, `skeleton`. The daemon runs the query
|
|
7
|
+
* against its already-warm VectorDB and returns the finished answer; the client
|
|
8
|
+
* renders it and does anything tied to its own cwd, git, or filesystem.
|
|
9
|
+
*
|
|
10
|
+
* Every exported `run*` function is the *shared* implementation: the verb
|
|
11
|
+
* handler calls it with the daemon's VectorDB, and the command's in-process
|
|
12
|
+
* fallback calls the same function with a VectorDB it opened itself. That is
|
|
13
|
+
* what makes the two paths byte-identical — there is only one implementation of
|
|
14
|
+
* each query and each aggregation, and neither path can drift from the other.
|
|
15
|
+
*
|
|
16
|
+
* Two things are deliberately *not* raw row proxies:
|
|
17
|
+
*
|
|
18
|
+
* - `rows.symbols` returns the aggregated symbol table, not the 2000–10000 rows
|
|
19
|
+
* the select produces. The aggregation is pure, so moving it daemon-side
|
|
20
|
+
* cannot change the answer, and it turns a multi-megabyte JSON line into a
|
|
21
|
+
* list bounded by `--limit`.
|
|
22
|
+
* - `rows.project` likewise returns the overview, not the rows. `project.ts`
|
|
23
|
+
* selects up to 200 000 rows including two symbol-array columns; on the
|
|
24
|
+
* platform project (266 753 chunks) proxying those rows would push hundreds
|
|
25
|
+
* of megabytes through the daemon's event loop for a summary that fits in a
|
|
26
|
+
* few kilobytes. The plan's own reasoning — "verbs at the library-function
|
|
27
|
+
* boundary, not a row proxy" — applies here more than anywhere.
|
|
28
|
+
*
|
|
29
|
+
* `rows.locate` *is* a row select, because its callers need the rows: extract
|
|
30
|
+
* reads the file body at the returned line range, log feeds the paths to git,
|
|
31
|
+
* related counts them. It is batched (an array of matchers, one response) so
|
|
32
|
+
* `related`'s per-symbol loop costs one round trip instead of hundreds.
|
|
33
|
+
*/
|
|
34
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
35
|
+
if (k2 === undefined) k2 = k;
|
|
36
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
37
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
38
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
39
|
+
}
|
|
40
|
+
Object.defineProperty(o, k2, desc);
|
|
41
|
+
}) : (function(o, m, k, k2) {
|
|
42
|
+
if (k2 === undefined) k2 = k;
|
|
43
|
+
o[k2] = m[k];
|
|
44
|
+
}));
|
|
45
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
46
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
47
|
+
}) : function(o, v) {
|
|
48
|
+
o["default"] = v;
|
|
49
|
+
});
|
|
50
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
51
|
+
var ownKeys = function(o) {
|
|
52
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
53
|
+
var ar = [];
|
|
54
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
55
|
+
return ar;
|
|
56
|
+
};
|
|
57
|
+
return ownKeys(o);
|
|
58
|
+
};
|
|
59
|
+
return function (mod) {
|
|
60
|
+
if (mod && mod.__esModule) return mod;
|
|
61
|
+
var result = {};
|
|
62
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
63
|
+
__setModuleDefault(result, mod);
|
|
64
|
+
return result;
|
|
65
|
+
};
|
|
66
|
+
})();
|
|
67
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
68
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
69
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
70
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
71
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
72
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
73
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
74
|
+
});
|
|
75
|
+
};
|
|
76
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
77
|
+
exports.MAX_LOCATE_ROWS = exports.ReadVerbError = void 0;
|
|
78
|
+
exports.withLocalStore = withLocalStore;
|
|
79
|
+
exports.scopeToWire = scopeToWire;
|
|
80
|
+
exports.assertReadableProject = assertReadableProject;
|
|
81
|
+
exports.resolveWireScope = resolveWireScope;
|
|
82
|
+
exports.clampInt = clampInt;
|
|
83
|
+
exports.asReadVerbResponse = asReadVerbResponse;
|
|
84
|
+
exports.runSymbols = runSymbols;
|
|
85
|
+
exports.handleRowsSymbols = handleRowsSymbols;
|
|
86
|
+
exports.runProject = runProject;
|
|
87
|
+
exports.handleRowsProject = handleRowsProject;
|
|
88
|
+
exports.runLocate = runLocate;
|
|
89
|
+
exports.handleRowsLocate = handleRowsLocate;
|
|
90
|
+
exports.runSkeleton = runSkeleton;
|
|
91
|
+
exports.findFileBySymbol = findFileBySymbol;
|
|
92
|
+
exports.readRows = readRows;
|
|
93
|
+
exports.handleRowsSkeleton = handleRowsSkeleton;
|
|
94
|
+
exports.registerRowsVerbs = registerRowsVerbs;
|
|
95
|
+
const callsites_1 = require("../graph/callsites");
|
|
96
|
+
const retriever_1 = require("../skeleton/retriever");
|
|
97
|
+
const arrow_1 = require("../utils/arrow");
|
|
98
|
+
const filter_builder_1 = require("../utils/filter-builder");
|
|
99
|
+
const path_containment_1 = require("../utils/path-containment");
|
|
100
|
+
const project_registry_1 = require("../utils/project-registry");
|
|
101
|
+
const query_timeout_1 = require("../utils/query-timeout");
|
|
102
|
+
const scope_filter_1 = require("../utils/scope-filter");
|
|
103
|
+
const read_verbs_1 = require("./read-verbs");
|
|
104
|
+
/**
|
|
105
|
+
* Client-side counterpart to `readStoreDeps`: open a private VectorDB, run one
|
|
106
|
+
* of the shared `run*` functions against it, and close it again.
|
|
107
|
+
*
|
|
108
|
+
* This is the *only* place a read command opens the store, and it is reached
|
|
109
|
+
* only from a `withStoreRead` `inProcess` callback — i.e. only when nothing is
|
|
110
|
+
* listening on the daemon socket.
|
|
111
|
+
*/
|
|
112
|
+
function withLocalStore(lancedbDir, fn) {
|
|
113
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
114
|
+
const { VectorDB } = yield Promise.resolve().then(() => __importStar(require("../store/vector-db")));
|
|
115
|
+
const db = new VectorDB(lancedbDir);
|
|
116
|
+
try {
|
|
117
|
+
return yield fn({ vectorDb: db });
|
|
118
|
+
}
|
|
119
|
+
finally {
|
|
120
|
+
try {
|
|
121
|
+
yield db.close();
|
|
122
|
+
}
|
|
123
|
+
catch (_a) { }
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
function scopeToWire(scope) {
|
|
128
|
+
return {
|
|
129
|
+
pathPrefix: scope.pathPrefix,
|
|
130
|
+
inPrefixes: scope.inPrefixes,
|
|
131
|
+
excludePrefixes: scope.excludePrefixes,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
/** Thrown for a payload the daemon refuses; caught into an error response. */
|
|
135
|
+
class ReadVerbError extends Error {
|
|
136
|
+
constructor(message, hint) {
|
|
137
|
+
super(message);
|
|
138
|
+
this.name = "ReadVerbError";
|
|
139
|
+
this.hint = hint;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
exports.ReadVerbError = ReadVerbError;
|
|
143
|
+
/**
|
|
144
|
+
* Validate `projectRoot` the way the `search` case does: it must name a project
|
|
145
|
+
* the registry knows and has not marked `error`. An unregistered directory gets
|
|
146
|
+
* the same "run: gmax add" answer `search` already gives it, rather than a
|
|
147
|
+
* silently empty result from a scoped scan of the shared table.
|
|
148
|
+
*/
|
|
149
|
+
function assertReadableProject(projectRoot) {
|
|
150
|
+
if (typeof projectRoot !== "string" || projectRoot === "") {
|
|
151
|
+
throw new ReadVerbError("missing projectRoot");
|
|
152
|
+
}
|
|
153
|
+
const eligible = (0, project_registry_1.listProjects)().filter((p) => p.status !== "error");
|
|
154
|
+
if (!eligible.some((p) => p.root === projectRoot)) {
|
|
155
|
+
throw new ReadVerbError("project not registered", `run: gmax add ${projectRoot}`);
|
|
156
|
+
}
|
|
157
|
+
return projectRoot;
|
|
158
|
+
}
|
|
159
|
+
/** Resolve one wire prefix against the project root, keeping its trailing slash. */
|
|
160
|
+
function containedPrefix(projectRoot, prefix) {
|
|
161
|
+
if (typeof prefix !== "string" || prefix === "") {
|
|
162
|
+
throw new ReadVerbError("invalid path prefix");
|
|
163
|
+
}
|
|
164
|
+
const resolved = (0, path_containment_1.resolveContainedPath)(projectRoot, prefix);
|
|
165
|
+
// resolveContainedPath goes through path.resolve, which drops the trailing
|
|
166
|
+
// separator that makes a prefix a directory boundary. Put it back only when
|
|
167
|
+
// the caller had one — `symbols --path src` deliberately matches
|
|
168
|
+
// `starts_with(path, '<root>/src')` with no trailing slash.
|
|
169
|
+
return prefix.endsWith("/") ? `${resolved.replace(/\/$/, "")}/` : resolved;
|
|
170
|
+
}
|
|
171
|
+
/** Re-validate a wire scope and rebuild the `ResolvedScope` the queries use. */
|
|
172
|
+
function resolveWireScope(projectRoot, wire) {
|
|
173
|
+
var _a, _b;
|
|
174
|
+
const raw = (wire !== null && wire !== void 0 ? wire : {});
|
|
175
|
+
const projectPrefix = projectRoot.endsWith("/")
|
|
176
|
+
? projectRoot
|
|
177
|
+
: `${projectRoot}/`;
|
|
178
|
+
return {
|
|
179
|
+
pathPrefix: raw.pathPrefix === undefined
|
|
180
|
+
? projectPrefix
|
|
181
|
+
: containedPrefix(projectRoot, raw.pathPrefix),
|
|
182
|
+
inPrefixes: ((_a = raw.inPrefixes) !== null && _a !== void 0 ? _a : []).map((p) => containedPrefix(projectRoot, p)),
|
|
183
|
+
excludePrefixes: ((_b = raw.excludePrefixes) !== null && _b !== void 0 ? _b : []).map((p) => containedPrefix(projectRoot, p)),
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
/** Clamp an integer payload field into `[min, max]`, falling back on garbage. */
|
|
187
|
+
function clampInt(value, fallback, min, max) {
|
|
188
|
+
const n = typeof value === "number"
|
|
189
|
+
? Math.trunc(value)
|
|
190
|
+
: Number.parseInt(String(value !== null && value !== void 0 ? value : ""), 10);
|
|
191
|
+
if (!Number.isFinite(n))
|
|
192
|
+
return fallback;
|
|
193
|
+
return Math.min(Math.max(n, min), max);
|
|
194
|
+
}
|
|
195
|
+
function tableOf(deps) {
|
|
196
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
197
|
+
var _a;
|
|
198
|
+
if (!deps.vectorDb)
|
|
199
|
+
throw new ReadVerbError("daemon not ready");
|
|
200
|
+
(_a = deps.touchActivity) === null || _a === void 0 ? void 0 : _a.call(deps);
|
|
201
|
+
return deps.vectorDb.ensureTable();
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
/** Wrap a handler body so a ReadVerbError becomes a clean error response. */
|
|
205
|
+
function asReadVerbResponse(fn) {
|
|
206
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
207
|
+
try {
|
|
208
|
+
return yield fn();
|
|
209
|
+
}
|
|
210
|
+
catch (err) {
|
|
211
|
+
if (err instanceof ReadVerbError) {
|
|
212
|
+
return err.hint
|
|
213
|
+
? { ok: false, error: err.message, hint: err.hint }
|
|
214
|
+
: { ok: false, error: err.message };
|
|
215
|
+
}
|
|
216
|
+
throw err;
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
function toStringArray(val) {
|
|
221
|
+
if (Array.isArray(val))
|
|
222
|
+
return val.filter((v) => typeof v === "string");
|
|
223
|
+
const arr = (0, arrow_1.toArr)(val);
|
|
224
|
+
return Array.isArray(arr) ? arr.filter((v) => typeof v === "string") : [];
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* The `symbols` select plus its aggregation. Same query and same ordering as
|
|
228
|
+
* before the split; only the process it runs in changed.
|
|
229
|
+
*
|
|
230
|
+
* `role` and `is_exported` were added to the select for MCP's `list_symbols`,
|
|
231
|
+
* which has always tagged each row with them. They cost nothing (the same rows,
|
|
232
|
+
* two more columns) and `gmax symbols` simply does not render them, so both
|
|
233
|
+
* callers keep the output they had.
|
|
234
|
+
*/
|
|
235
|
+
function runSymbols(deps, req) {
|
|
236
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
237
|
+
const table = yield tableOf(deps);
|
|
238
|
+
let query = table
|
|
239
|
+
.query()
|
|
240
|
+
.select(["defined_symbols", "path", "start_line", "role", "is_exported"])
|
|
241
|
+
.where("array_length(defined_symbols) > 0")
|
|
242
|
+
// Fetch more rows to ensure we have enough after filtering/aggregation
|
|
243
|
+
.limit(req.pattern ? 10000 : Math.max(req.limit * 50, 2000));
|
|
244
|
+
if (req.pathPrefix) {
|
|
245
|
+
query = query.where((0, filter_builder_1.pathStartsWith)((0, filter_builder_1.normalizePath)(req.pathPrefix)));
|
|
246
|
+
}
|
|
247
|
+
const rows = yield query.toArray();
|
|
248
|
+
const map = new Map();
|
|
249
|
+
for (const row of rows) {
|
|
250
|
+
const defs = toStringArray(row.defined_symbols);
|
|
251
|
+
const rowPath = String(row.path || "");
|
|
252
|
+
const line = Number(row.start_line || 0);
|
|
253
|
+
const role = String(row.role || "");
|
|
254
|
+
const exported = Boolean(row.is_exported);
|
|
255
|
+
for (const sym of defs) {
|
|
256
|
+
if (req.pattern &&
|
|
257
|
+
!sym.toLowerCase().includes(req.pattern.toLowerCase())) {
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
const existing = map.get(sym);
|
|
261
|
+
if (existing) {
|
|
262
|
+
existing.count += 1;
|
|
263
|
+
}
|
|
264
|
+
else {
|
|
265
|
+
map.set(sym, {
|
|
266
|
+
symbol: sym,
|
|
267
|
+
count: 1,
|
|
268
|
+
path: rowPath,
|
|
269
|
+
line,
|
|
270
|
+
role,
|
|
271
|
+
exported,
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
return Array.from(map.values())
|
|
277
|
+
.sort((a, b) => {
|
|
278
|
+
// Sort by count desc, then symbol asc
|
|
279
|
+
if (b.count !== a.count)
|
|
280
|
+
return b.count - a.count;
|
|
281
|
+
return a.symbol.localeCompare(b.symbol);
|
|
282
|
+
})
|
|
283
|
+
.slice(0, req.limit);
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
function handleRowsSymbols(deps, payload) {
|
|
287
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
288
|
+
return asReadVerbResponse(() => __awaiter(this, void 0, void 0, function* () {
|
|
289
|
+
const projectRoot = assertReadableProject(payload.projectRoot);
|
|
290
|
+
const entries = yield runSymbols(deps, {
|
|
291
|
+
projectRoot,
|
|
292
|
+
pathPrefix: payload.pathPrefix === undefined
|
|
293
|
+
? undefined
|
|
294
|
+
: containedPrefix(projectRoot, payload.pathPrefix),
|
|
295
|
+
pattern: typeof payload.pattern === "string" && payload.pattern
|
|
296
|
+
? payload.pattern
|
|
297
|
+
: undefined,
|
|
298
|
+
limit: clampInt(payload.limit, 20, 1, 10000),
|
|
299
|
+
});
|
|
300
|
+
return { ok: true, entries };
|
|
301
|
+
}));
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* The `project` scan and its aggregation. Returns the finished overview rather
|
|
306
|
+
* than the (up to 200 000) rows it was computed from — see the file header.
|
|
307
|
+
*/
|
|
308
|
+
function runProject(deps, projectRoot) {
|
|
309
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
310
|
+
const table = yield tableOf(deps);
|
|
311
|
+
const prefix = projectRoot.endsWith("/") ? projectRoot : `${projectRoot}/`;
|
|
312
|
+
const rows = yield table
|
|
313
|
+
.query()
|
|
314
|
+
.select([
|
|
315
|
+
"path",
|
|
316
|
+
"role",
|
|
317
|
+
"is_exported",
|
|
318
|
+
"complexity",
|
|
319
|
+
"defined_symbols",
|
|
320
|
+
"referenced_symbols",
|
|
321
|
+
])
|
|
322
|
+
.where((0, filter_builder_1.pathStartsWith)(prefix))
|
|
323
|
+
.limit(200000)
|
|
324
|
+
.toArray();
|
|
325
|
+
const nodePath = yield Promise.resolve().then(() => __importStar(require("node:path")));
|
|
326
|
+
const files = new Set();
|
|
327
|
+
const extCounts = new Map();
|
|
328
|
+
const dirCounts = new Map();
|
|
329
|
+
const roleCounts = new Map();
|
|
330
|
+
const symbolRefs = new Map();
|
|
331
|
+
const definedInProject = new Set();
|
|
332
|
+
const entryPoints = [];
|
|
333
|
+
const seenEntryPoints = new Set();
|
|
334
|
+
for (const row of rows) {
|
|
335
|
+
const r = row;
|
|
336
|
+
const p = String(r.path || "");
|
|
337
|
+
const role = String(r.role || "IMPLEMENTATION");
|
|
338
|
+
const exported = Boolean(r.is_exported);
|
|
339
|
+
const complexity = Number(r.complexity || 0);
|
|
340
|
+
const defs = (0, arrow_1.toArr)(r.defined_symbols);
|
|
341
|
+
const refs = (0, arrow_1.toArr)(r.referenced_symbols);
|
|
342
|
+
files.add(p);
|
|
343
|
+
const ext = nodePath.extname(p).toLowerCase() || nodePath.basename(p);
|
|
344
|
+
extCounts.set(ext, (extCounts.get(ext) || 0) + 1);
|
|
345
|
+
const rel = p.startsWith(prefix) ? p.slice(prefix.length) : p;
|
|
346
|
+
const parts = rel.split("/");
|
|
347
|
+
const dir = parts.length > 2
|
|
348
|
+
? `${parts.slice(0, 2).join("/")}/`
|
|
349
|
+
: parts.length > 1
|
|
350
|
+
? `${parts[0]}/`
|
|
351
|
+
: "(root)";
|
|
352
|
+
if (!dirCounts.has(dir))
|
|
353
|
+
dirCounts.set(dir, { files: new Set(), chunks: 0 });
|
|
354
|
+
const dc = dirCounts.get(dir);
|
|
355
|
+
dc.files.add(p);
|
|
356
|
+
dc.chunks++;
|
|
357
|
+
roleCounts.set(role, (roleCounts.get(role) || 0) + 1);
|
|
358
|
+
for (const d of defs)
|
|
359
|
+
definedInProject.add(d);
|
|
360
|
+
for (const ref of refs)
|
|
361
|
+
symbolRefs.set(ref, (symbolRefs.get(ref) || 0) + 1);
|
|
362
|
+
if (exported &&
|
|
363
|
+
role === "ORCHESTRATION" &&
|
|
364
|
+
complexity >= 5 &&
|
|
365
|
+
defs.length > 0) {
|
|
366
|
+
const epKey = `${defs[0]}:${p}`;
|
|
367
|
+
if (!seenEntryPoints.has(epKey)) {
|
|
368
|
+
seenEntryPoints.add(epKey);
|
|
369
|
+
entryPoints.push({
|
|
370
|
+
symbol: defs[0],
|
|
371
|
+
path: p.startsWith(prefix) ? p.slice(prefix.length) : p,
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
return {
|
|
377
|
+
chunks: rows.length,
|
|
378
|
+
files: files.size,
|
|
379
|
+
extEntries: Array.from(extCounts.entries())
|
|
380
|
+
.sort((a, b) => b[1] - a[1])
|
|
381
|
+
.slice(0, 8),
|
|
382
|
+
// Human output shows 12, agent output shows the first 8 of the same order.
|
|
383
|
+
dirEntries: Array.from(dirCounts.entries())
|
|
384
|
+
.sort((a, b) => b[1].chunks - a[1].chunks)
|
|
385
|
+
.slice(0, 12)
|
|
386
|
+
.map(([dir, data]) => [dir, { files: data.files.size, chunks: data.chunks }]),
|
|
387
|
+
roleEntries: Array.from(roleCounts.entries()).sort((a, b) => b[1] - a[1]),
|
|
388
|
+
// Key symbols must be the project's own: raw referenced_symbols counts
|
|
389
|
+
// are dominated by JS builtins (push, slice, map, …) which say nothing
|
|
390
|
+
// about the codebase.
|
|
391
|
+
topSymbols: Array.from(symbolRefs.entries())
|
|
392
|
+
.filter(([s]) => definedInProject.has(s) && !(0, callsites_1.isBuiltinCallee)(s))
|
|
393
|
+
.sort((a, b) => b[1] - a[1])
|
|
394
|
+
.slice(0, 8),
|
|
395
|
+
entryPoints: entryPoints.slice(0, 10),
|
|
396
|
+
};
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
function handleRowsProject(deps, payload) {
|
|
400
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
401
|
+
return asReadVerbResponse(() => __awaiter(this, void 0, void 0, function* () {
|
|
402
|
+
const projectRoot = assertReadableProject(payload.projectRoot);
|
|
403
|
+
return { ok: true, overview: yield runProject(deps, projectRoot) };
|
|
404
|
+
}));
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
// --- rows.locate ------------------------------------------------------------
|
|
408
|
+
/** The column set a row select may ask for. `content` and `vector` are not on it. */
|
|
409
|
+
const LOCATE_COLUMNS = new Set([
|
|
410
|
+
"path",
|
|
411
|
+
"start_line",
|
|
412
|
+
"end_line",
|
|
413
|
+
"role",
|
|
414
|
+
"is_exported",
|
|
415
|
+
"complexity",
|
|
416
|
+
"defined_symbols",
|
|
417
|
+
"referenced_symbols",
|
|
418
|
+
"parent_symbol",
|
|
419
|
+
"chunk_type",
|
|
420
|
+
"id",
|
|
421
|
+
]);
|
|
422
|
+
const ARRAY_COLUMNS = new Set(["defined_symbols", "referenced_symbols"]);
|
|
423
|
+
/**
|
|
424
|
+
* Upper bound on rows one matcher may return when the caller sets no limit.
|
|
425
|
+
* `related`'s `content LIKE` scan deliberately has no `.limit()` — the limit
|
|
426
|
+
* pushdown deadlocks on that shape (see query-timeout.ts) — and its JS loop
|
|
427
|
+
* caps at `--limit` distinct paths, so a wire cap only bites on a query that
|
|
428
|
+
* would already have been unreasonable to render.
|
|
429
|
+
*/
|
|
430
|
+
exports.MAX_LOCATE_ROWS = 5000;
|
|
431
|
+
function matchCondition(match) {
|
|
432
|
+
switch (match.kind) {
|
|
433
|
+
case "definedSymbol":
|
|
434
|
+
return {
|
|
435
|
+
where: `array_contains(defined_symbols, '${(0, filter_builder_1.escapeSqlString)(match.symbol)}')`,
|
|
436
|
+
label: `defined_symbols ∋ ${match.symbol}`,
|
|
437
|
+
};
|
|
438
|
+
case "referencedSymbol":
|
|
439
|
+
return {
|
|
440
|
+
where: `array_contains(referenced_symbols, '${(0, filter_builder_1.escapeSqlString)(match.symbol)}')`,
|
|
441
|
+
label: `referenced_symbols ∋ ${match.symbol}`,
|
|
442
|
+
};
|
|
443
|
+
case "path":
|
|
444
|
+
return {
|
|
445
|
+
where: `path = '${(0, filter_builder_1.escapeSqlString)(match.path)}'`,
|
|
446
|
+
label: `path = ${match.path}`,
|
|
447
|
+
};
|
|
448
|
+
case "contentLike":
|
|
449
|
+
return {
|
|
450
|
+
where: `content LIKE '%${(0, filter_builder_1.escapeSqlString)(match.value)}%'`,
|
|
451
|
+
label: `content LIKE %${match.value}% (related mentions)`,
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* Normalize one Lance row to plain JSON. Arrow list columns do not survive
|
|
457
|
+
* JSON.stringify as arrays, so they are converted here — in the *shared*
|
|
458
|
+
* function, which means the in-process path sees exactly the same shape the
|
|
459
|
+
* socket delivers.
|
|
460
|
+
*/
|
|
461
|
+
function normalizeRow(row, select) {
|
|
462
|
+
const src = row;
|
|
463
|
+
const out = {};
|
|
464
|
+
for (const col of select) {
|
|
465
|
+
const value = src[col];
|
|
466
|
+
if (ARRAY_COLUMNS.has(col))
|
|
467
|
+
out[col] = (0, arrow_1.toArr)(value);
|
|
468
|
+
else if (col === "is_exported")
|
|
469
|
+
out[col] = Boolean(value);
|
|
470
|
+
else if (col === "start_line" || col === "end_line" || col === "complexity")
|
|
471
|
+
out[col] = Number(value !== null && value !== void 0 ? value : 0);
|
|
472
|
+
else
|
|
473
|
+
out[col] = value === undefined || value === null ? "" : String(value);
|
|
474
|
+
}
|
|
475
|
+
return out;
|
|
476
|
+
}
|
|
477
|
+
/** Run a batch of row selects, one result array per matcher, order preserved. */
|
|
478
|
+
function runLocate(deps, req) {
|
|
479
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
480
|
+
var _a;
|
|
481
|
+
const table = yield tableOf(deps);
|
|
482
|
+
const scoped = req.scoped !== false;
|
|
483
|
+
const pathScope = (0, scope_filter_1.buildScopeWhere)(req.scope);
|
|
484
|
+
const results = [];
|
|
485
|
+
for (const match of req.matches) {
|
|
486
|
+
const { where, label } = matchCondition(match);
|
|
487
|
+
let query = table
|
|
488
|
+
.query()
|
|
489
|
+
.select(req.select)
|
|
490
|
+
.where(scoped ? `${where} AND ${pathScope}` : where);
|
|
491
|
+
if (req.limit !== undefined)
|
|
492
|
+
query = query.limit(req.limit);
|
|
493
|
+
const rows = yield (0, query_timeout_1.withQueryTimeout)(query.toArray(), label);
|
|
494
|
+
results.push(rows
|
|
495
|
+
.slice(0, (_a = req.limit) !== null && _a !== void 0 ? _a : exports.MAX_LOCATE_ROWS)
|
|
496
|
+
.map((row) => normalizeRow(row, req.select)));
|
|
497
|
+
}
|
|
498
|
+
return results;
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
function handleRowsLocate(deps, payload) {
|
|
502
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
503
|
+
return asReadVerbResponse(() => __awaiter(this, void 0, void 0, function* () {
|
|
504
|
+
const projectRoot = assertReadableProject(payload.projectRoot);
|
|
505
|
+
const scope = resolveWireScope(projectRoot, payload.scope);
|
|
506
|
+
const select = Array.isArray(payload.select)
|
|
507
|
+
? payload.select.filter((c) => typeof c === "string" && LOCATE_COLUMNS.has(c))
|
|
508
|
+
: [];
|
|
509
|
+
if (select.length === 0)
|
|
510
|
+
throw new ReadVerbError("invalid select");
|
|
511
|
+
const rawMatches = Array.isArray(payload.matches) ? payload.matches : [];
|
|
512
|
+
if (rawMatches.length === 0)
|
|
513
|
+
throw new ReadVerbError("missing matches");
|
|
514
|
+
if (rawMatches.length > 2000)
|
|
515
|
+
throw new ReadVerbError("too many matches");
|
|
516
|
+
const matches = rawMatches.map((m) => {
|
|
517
|
+
const match = m;
|
|
518
|
+
if ((match.kind === "definedSymbol" || match.kind === "referencedSymbol") &&
|
|
519
|
+
typeof match.symbol === "string") {
|
|
520
|
+
return match;
|
|
521
|
+
}
|
|
522
|
+
if (match.kind === "path" &&
|
|
523
|
+
typeof match.path === "string") {
|
|
524
|
+
// A path matcher is an exact equality on an indexed absolute path;
|
|
525
|
+
// containment keeps it inside the project the caller named.
|
|
526
|
+
return {
|
|
527
|
+
kind: "path",
|
|
528
|
+
path: (0, path_containment_1.resolveContainedPath)(projectRoot, match.path),
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
if (match.kind === "contentLike" &&
|
|
532
|
+
typeof match.value === "string") {
|
|
533
|
+
return match;
|
|
534
|
+
}
|
|
535
|
+
throw new ReadVerbError("invalid match");
|
|
536
|
+
});
|
|
537
|
+
const rows = yield runLocate(deps, {
|
|
538
|
+
projectRoot,
|
|
539
|
+
scope,
|
|
540
|
+
select,
|
|
541
|
+
matches,
|
|
542
|
+
limit: payload.limit === undefined
|
|
543
|
+
? undefined
|
|
544
|
+
: clampInt(payload.limit, 10, 1, exports.MAX_LOCATE_ROWS),
|
|
545
|
+
scoped: payload.scoped !== false,
|
|
546
|
+
});
|
|
547
|
+
return { ok: true, rows };
|
|
548
|
+
}));
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
/**
|
|
552
|
+
* `skeleton`'s two store reads: the FTS symbol → file lookup and the stored
|
|
553
|
+
* per-file skeleton. Both are best-effort and return null rather than throwing,
|
|
554
|
+
* exactly as `findFileBySymbol` and `getStoredSkeleton` always have.
|
|
555
|
+
*/
|
|
556
|
+
function runSkeleton(deps, req) {
|
|
557
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
558
|
+
var _a, _b;
|
|
559
|
+
if (!deps.vectorDb)
|
|
560
|
+
throw new ReadVerbError("daemon not ready");
|
|
561
|
+
(_a = deps.touchActivity) === null || _a === void 0 ? void 0 : _a.call(deps);
|
|
562
|
+
const db = deps.vectorDb;
|
|
563
|
+
let filePath = (_b = req.path) !== null && _b !== void 0 ? _b : null;
|
|
564
|
+
if (!filePath && req.symbol) {
|
|
565
|
+
filePath = yield findFileBySymbol(db, req.symbol, req.projectRoot);
|
|
566
|
+
}
|
|
567
|
+
if (!filePath)
|
|
568
|
+
return { path: null, skeleton: null };
|
|
569
|
+
return { path: filePath, skeleton: yield (0, retriever_1.getStoredSkeleton)(db, filePath) };
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
/** FTS lookup for the file that defines `symbol`; null when nothing matches. */
|
|
573
|
+
function findFileBySymbol(db, symbol, projectRoot) {
|
|
574
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
575
|
+
try {
|
|
576
|
+
const table = yield db.ensureTable();
|
|
577
|
+
const results = yield table
|
|
578
|
+
.search(symbol)
|
|
579
|
+
.where((0, filter_builder_1.pathStartsWith)(`${projectRoot}/`))
|
|
580
|
+
.limit(10)
|
|
581
|
+
.toArray();
|
|
582
|
+
for (const result of results) {
|
|
583
|
+
const defined = result.defined_symbols;
|
|
584
|
+
if ((0, arrow_1.toArr)(defined).includes(symbol)) {
|
|
585
|
+
return String(result.path);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
if (results.length > 0) {
|
|
589
|
+
return String(results[0].path);
|
|
590
|
+
}
|
|
591
|
+
return null;
|
|
592
|
+
}
|
|
593
|
+
catch (_a) {
|
|
594
|
+
return null;
|
|
595
|
+
}
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
/**
|
|
599
|
+
* Client side of `rows.locate`: one round trip for a whole batch of matchers,
|
|
600
|
+
* falling back to the same `runLocate` in-process when no daemon is listening.
|
|
601
|
+
* Shared by extract, related, log, and context so the wire shape has exactly
|
|
602
|
+
* one producer and one consumer.
|
|
603
|
+
*/
|
|
604
|
+
function readRows(opts) {
|
|
605
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
606
|
+
if (opts.matches.length === 0)
|
|
607
|
+
return [];
|
|
608
|
+
const { sendDaemonCommand } = yield Promise.resolve().then(() => __importStar(require("../utils/daemon-client")));
|
|
609
|
+
const { withStoreRead } = yield Promise.resolve().then(() => __importStar(require("../utils/store-access")));
|
|
610
|
+
return withStoreRead(opts.name, Object.assign({ daemon: () => {
|
|
611
|
+
var _a;
|
|
612
|
+
return sendDaemonCommand({
|
|
613
|
+
cmd: "rows.locate",
|
|
614
|
+
projectRoot: opts.projectRoot,
|
|
615
|
+
scope: scopeToWire(opts.scope),
|
|
616
|
+
select: opts.select,
|
|
617
|
+
matches: opts.matches,
|
|
618
|
+
limit: opts.limit,
|
|
619
|
+
scoped: opts.scoped,
|
|
620
|
+
}, { timeoutMs: (_a = opts.timeoutMs) !== null && _a !== void 0 ? _a : 60000 });
|
|
621
|
+
}, render: (resp) => { var _a; return ((_a = resp.rows) !== null && _a !== void 0 ? _a : []); }, inProcess: () => withLocalStore(opts.lancedbDir, (deps) => runLocate(deps, {
|
|
622
|
+
projectRoot: opts.projectRoot,
|
|
623
|
+
scope: opts.scope,
|
|
624
|
+
select: opts.select,
|
|
625
|
+
matches: opts.matches,
|
|
626
|
+
limit: opts.limit,
|
|
627
|
+
scoped: opts.scoped,
|
|
628
|
+
})), fallbackOnUnknownVerb: true }, opts.fallback));
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
function handleRowsSkeleton(deps, payload) {
|
|
632
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
633
|
+
return asReadVerbResponse(() => __awaiter(this, void 0, void 0, function* () {
|
|
634
|
+
const projectRoot = assertReadableProject(payload.projectRoot);
|
|
635
|
+
const target = typeof payload.path === "string" && payload.path
|
|
636
|
+
? (0, path_containment_1.resolveContainedPath)(projectRoot, payload.path)
|
|
637
|
+
: undefined;
|
|
638
|
+
const symbol = typeof payload.symbol === "string" && payload.symbol
|
|
639
|
+
? payload.symbol
|
|
640
|
+
: undefined;
|
|
641
|
+
if (!target && !symbol)
|
|
642
|
+
throw new ReadVerbError("missing path or symbol");
|
|
643
|
+
const result = yield runSkeleton(deps, {
|
|
644
|
+
projectRoot,
|
|
645
|
+
path: target,
|
|
646
|
+
symbol,
|
|
647
|
+
});
|
|
648
|
+
return Object.assign({ ok: true }, result);
|
|
649
|
+
}));
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
// --- Registration -----------------------------------------------------------
|
|
653
|
+
/**
|
|
654
|
+
* Called from `Daemon.start()`, not at module scope: the registry's exact
|
|
655
|
+
* contents are asserted on in tests/ipc-read-verbs.test.ts, and registering on
|
|
656
|
+
* import would make it non-empty the moment ipc-handler is loaded.
|
|
657
|
+
*/
|
|
658
|
+
function registerRowsVerbs() {
|
|
659
|
+
(0, read_verbs_1.registerReadVerbs)({
|
|
660
|
+
"rows.symbols": (payload, ctx) => handleRowsSymbols(ctx.daemon.storeReadDeps(), payload),
|
|
661
|
+
"rows.project": (payload, ctx) => handleRowsProject(ctx.daemon.storeReadDeps(), payload),
|
|
662
|
+
"rows.locate": (payload, ctx) => handleRowsLocate(ctx.daemon.storeReadDeps(), payload),
|
|
663
|
+
"rows.skeleton": (payload, ctx) => handleRowsSkeleton(ctx.daemon.storeReadDeps(), payload),
|
|
664
|
+
});
|
|
665
|
+
}
|