grepmax 0.26.27 → 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/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 +1 -1
- package/plugins/grepmax/.claude-plugin/plugin.json +1 -1
package/dist/commands/mcp.js
CHANGED
|
@@ -53,6 +53,7 @@ exports.mcp = void 0;
|
|
|
53
53
|
exports.toStringArray = toStringArray;
|
|
54
54
|
exports.ok = ok;
|
|
55
55
|
exports.err = err;
|
|
56
|
+
exports.isMcpSessionFallback = isMcpSessionFallback;
|
|
56
57
|
exports.shouldFallbackMcpDaemonSearch = shouldFallbackMcpDaemonSearch;
|
|
57
58
|
exports.isExplicitCrossProjectSearch = isExplicitCrossProjectSearch;
|
|
58
59
|
exports.resolveMcpProject = resolveMcpProject;
|
|
@@ -74,32 +75,30 @@ const commander_1 = require("commander");
|
|
|
74
75
|
const zod_1 = require("zod");
|
|
75
76
|
const config_1 = require("../config");
|
|
76
77
|
const surprising_connections_1 = require("../lib/analysis/surprising-connections");
|
|
77
|
-
const
|
|
78
|
-
const
|
|
78
|
+
const graph_handler_1 = require("../lib/daemon/graph-handler");
|
|
79
|
+
const rows_handler_1 = require("../lib/daemon/rows-handler");
|
|
80
|
+
const vector_handler_1 = require("../lib/daemon/vector-handler");
|
|
79
81
|
const embedding_status_1 = require("../lib/index/embedding-status");
|
|
80
82
|
const index_config_1 = require("../lib/index/index-config");
|
|
81
83
|
const syncer_1 = require("../lib/index/syncer");
|
|
82
84
|
const agent_search_formatter_1 = require("../lib/output/agent-search-formatter");
|
|
83
85
|
const searcher_1 = require("../lib/search/searcher");
|
|
84
86
|
const annotator_1 = require("../lib/skeleton/annotator");
|
|
85
|
-
const retriever_1 = require("../lib/skeleton/retriever");
|
|
86
87
|
const skeletonizer_1 = require("../lib/skeleton/skeletonizer");
|
|
87
88
|
const symbol_extractor_1 = require("../lib/skeleton/symbol-extractor");
|
|
88
|
-
const ann_config_1 = require("../lib/store/ann-config");
|
|
89
89
|
const meta_cache_1 = require("../lib/store/meta-cache");
|
|
90
|
-
const vector_db_1 = require("../lib/store/vector-db");
|
|
91
90
|
const cross_project_1 = require("../lib/utils/cross-project");
|
|
91
|
+
const daemon_client_1 = require("../lib/utils/daemon-client");
|
|
92
92
|
const file_utils_1 = require("../lib/utils/file-utils");
|
|
93
|
-
const filter_builder_1 = require("../lib/utils/filter-builder");
|
|
94
93
|
const format_helpers_1 = require("../lib/utils/format-helpers");
|
|
95
94
|
const import_extractor_1 = require("../lib/utils/import-extractor");
|
|
96
95
|
const path_containment_1 = require("../lib/utils/path-containment");
|
|
97
96
|
const project_registry_1 = require("../lib/utils/project-registry");
|
|
98
97
|
const project_root_1 = require("../lib/utils/project-root");
|
|
99
98
|
const scope_filter_1 = require("../lib/utils/scope-filter");
|
|
99
|
+
const store_access_1 = require("../lib/utils/store-access");
|
|
100
100
|
const watcher_launcher_1 = require("../lib/utils/watcher-launcher");
|
|
101
101
|
const watcher_store_1 = require("../lib/utils/watcher-store");
|
|
102
|
-
const audit_1 = require("./audit");
|
|
103
102
|
// ---------------------------------------------------------------------------
|
|
104
103
|
// Helpers
|
|
105
104
|
// ---------------------------------------------------------------------------
|
|
@@ -123,14 +122,50 @@ function ok(text) {
|
|
|
123
122
|
function err(text) {
|
|
124
123
|
return { content: [{ type: "text", text }], isError: true };
|
|
125
124
|
}
|
|
126
|
-
|
|
125
|
+
/**
|
|
126
|
+
* The two daemon answers that mean "not usable *yet*", as opposed to "your
|
|
127
|
+
* request was wrong".
|
|
128
|
+
*
|
|
129
|
+
* `project not watched` is the daemon still picking a project up; `daemon not
|
|
130
|
+
* ready` is its store still opening. A CLI command reports both — there is a
|
|
131
|
+
* user at a prompt who can wait or re-run — but an MCP tool call gets no second
|
|
132
|
+
* attempt, so it degrades to its own store read for the life of that one call.
|
|
133
|
+
* That is why these live here and not in `withStoreRead`'s built-in set.
|
|
134
|
+
*/
|
|
135
|
+
function isMcpSessionFallback(error) {
|
|
127
136
|
const message = String(error !== null && error !== void 0 ? error : "").toLowerCase();
|
|
128
|
-
return (message
|
|
129
|
-
message
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
137
|
+
return (message.includes("project not watched") ||
|
|
138
|
+
message.includes("daemon not ready"));
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* MCP's read-tool fallback policy, in one object so every tool shares it.
|
|
142
|
+
*
|
|
143
|
+
* - `fallbackOnUnknownVerb` — a daemon older than this build does not know the
|
|
144
|
+
* read verbs (the live 0.26.27 daemon does not); answer from the store rather
|
|
145
|
+
* than failing the tool call. Removed one release after the verbs ship.
|
|
146
|
+
* - `fallbackOnOversize` — the daemon's 2 MB response cap. See
|
|
147
|
+
* `isOversizeError` for why MCP opts in and the CLI does not.
|
|
148
|
+
* - `extraFallback` — the two session-readiness answers above.
|
|
149
|
+
*
|
|
150
|
+
* Everything else (`DAEMON_BUSY`, timeouts, scope rejections, store failures) is
|
|
151
|
+
* a live daemon saying no, and is reported. Falling back there would put a
|
|
152
|
+
* second opener on the store while the daemon is working.
|
|
153
|
+
*/
|
|
154
|
+
const MCP_STORE_FALLBACK = {
|
|
155
|
+
fallbackOnUnknownVerb: true,
|
|
156
|
+
fallbackOnOversize: true,
|
|
157
|
+
extraFallback: isMcpSessionFallback,
|
|
158
|
+
};
|
|
159
|
+
/**
|
|
160
|
+
* Kept as a named predicate because `daemonSearch`'s callers and its tests read
|
|
161
|
+
* as one rule; the rule itself is now assembled from `withStoreRead`'s
|
|
162
|
+
* classification rather than restated as a second string table.
|
|
163
|
+
*/
|
|
164
|
+
function shouldFallbackMcpDaemonSearch(error) {
|
|
165
|
+
return ((0, store_access_1.classifyDaemonError)(error) === "no-daemon" ||
|
|
166
|
+
(0, store_access_1.isUnknownCommandError)(error) ||
|
|
167
|
+
(0, store_access_1.isOversizeError)(error) ||
|
|
168
|
+
isMcpSessionFallback(error));
|
|
134
169
|
}
|
|
135
170
|
function isExplicitCrossProjectSearch(args, isSearchAll = false) {
|
|
136
171
|
return (isSearchAll ||
|
|
@@ -239,6 +274,11 @@ function formatMcpPointerSearchResults(data, displayRoot, options = {}) {
|
|
|
239
274
|
query: options.query,
|
|
240
275
|
});
|
|
241
276
|
}
|
|
277
|
+
/**
|
|
278
|
+
* Takes summary + findings rather than a whole `SurpriseAnalysisResult`: the
|
|
279
|
+
* `vector.surprises` verb drops the raw `pairs` array (the bulk of the object,
|
|
280
|
+
* and nothing renders it), so this is the shape both paths can supply.
|
|
281
|
+
*/
|
|
242
282
|
function formatMcpSurprisingConnections(result, top = 10) {
|
|
243
283
|
const { summary, findings } = result;
|
|
244
284
|
const lines = [
|
|
@@ -300,24 +340,15 @@ exports.mcp = new commander_1.Command("mcp")
|
|
|
300
340
|
.action((_optsArg, _cmd) => __awaiter(void 0, void 0, void 0, function* () {
|
|
301
341
|
process.title = "gmax-mcp";
|
|
302
342
|
// --- Lifecycle ---
|
|
303
|
-
|
|
304
|
-
|
|
343
|
+
// No per-session VectorDB. Every read tool goes through the daemon's read
|
|
344
|
+
// verbs, and the only store this process may open is the short-lived one
|
|
345
|
+
// `withLocalStore` opens and closes inside a `withStoreRead` fallback — so
|
|
346
|
+
// an MCP session holds no reader marker under ~/.gmax/lancedb.lease/readers/
|
|
347
|
+
// while a daemon serving the verbs is up.
|
|
305
348
|
let _skeletonizer = null;
|
|
306
349
|
let _indexReady = false;
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
try {
|
|
310
|
-
yield _vectorDb.close();
|
|
311
|
-
}
|
|
312
|
-
catch (_a) { }
|
|
313
|
-
_vectorDb = null;
|
|
314
|
-
_searcher = null;
|
|
315
|
-
}
|
|
316
|
-
});
|
|
317
|
-
const exit = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
318
|
-
yield cleanup();
|
|
319
|
-
process.exit(0);
|
|
320
|
-
});
|
|
350
|
+
// Nothing to close: the tools open no store of their own.
|
|
351
|
+
const exit = () => process.exit(0);
|
|
321
352
|
process.on("SIGINT", exit);
|
|
322
353
|
process.on("SIGTERM", exit);
|
|
323
354
|
// MCP SDK doesn't handle stdin close — exit when the client disconnects
|
|
@@ -339,38 +370,185 @@ exports.mcp = new commander_1.Command("mcp")
|
|
|
339
370
|
const paths = (0, project_root_1.ensureProjectPaths)(projectRoot);
|
|
340
371
|
// Propagate project root to worker processes
|
|
341
372
|
process.env.GMAX_PROJECT_ROOT = paths.root;
|
|
342
|
-
//
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
373
|
+
// --- Store access ---
|
|
374
|
+
/**
|
|
375
|
+
* One read through the access policy, with MCP's fallback allowances. The
|
|
376
|
+
* `inProcess` branch is entered only when no daemon serves the verb.
|
|
377
|
+
*/
|
|
378
|
+
function readStore(name, opts) {
|
|
379
|
+
return (0, store_access_1.withStoreRead)(name, Object.assign(Object.assign({}, opts), MCP_STORE_FALLBACK));
|
|
347
380
|
}
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
return _searcher;
|
|
381
|
+
/** Open the shared store for the duration of one fallback, then close it. */
|
|
382
|
+
function localStore(fn) {
|
|
383
|
+
return (0, rows_handler_1.withLocalStore)(paths.lancedbDir, fn);
|
|
352
384
|
}
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
385
|
+
/** A graph verb with MCP's fallback allowances. */
|
|
386
|
+
function graphVerb(verb, call) {
|
|
387
|
+
return (0, graph_handler_1.callGraphVerb)(verb, Object.assign(Object.assign({}, call), { fallback: MCP_STORE_FALLBACK }));
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* MCP tools take no `--in`/`--exclude`, so a tool's scope is always the
|
|
391
|
+
* whole project. Building it through `resolveScope` anyway keeps the wire
|
|
392
|
+
* shape identical to the CLI's.
|
|
393
|
+
*/
|
|
394
|
+
function projectScope(root) {
|
|
395
|
+
return (0, scope_filter_1.resolveScope)({ projectRoot: root });
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* Turn a thrown error into a tool result. A store-access refusal is already
|
|
399
|
+
* one actionable line, so it is passed through verbatim rather than buried
|
|
400
|
+
* under a `<Tool> failed:` prefix. It should never fire here — an MCP server
|
|
401
|
+
* is spawned by the editor, not from inside a sandboxed Bash tool — but the
|
|
402
|
+
* path exists so the message is right if it ever does.
|
|
403
|
+
*/
|
|
404
|
+
function toolError(prefix, e) {
|
|
405
|
+
if ((0, store_access_1.isStoreAccessRefused)(e))
|
|
406
|
+
return err(e.message);
|
|
407
|
+
return err(`${prefix}: ${e instanceof Error ? e.message : String(e)}`);
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* Search through the daemon, falling back to a Searcher over a store this
|
|
411
|
+
* process opens only for the duration of the call. Replaces the old
|
|
412
|
+
* `daemonSearch(...) ?? getSearcher().search(...)` pair, whose null return
|
|
413
|
+
* encoded the same fallback rule `withStoreRead` now owns.
|
|
414
|
+
*/
|
|
415
|
+
function searchStore(args) {
|
|
416
|
+
return readStore("search", {
|
|
417
|
+
daemon: () => {
|
|
418
|
+
var _a;
|
|
419
|
+
return (0, daemon_client_1.sendDaemonCommand)(Object.assign({ cmd: ((_a = args.filters) === null || _a === void 0 ? void 0 : _a.projectRoots) ? "search-v2" : "search" }, args), { timeoutMs: 60000 });
|
|
420
|
+
},
|
|
421
|
+
render: (resp) => ({
|
|
422
|
+
data: Array.isArray(resp.data) ? resp.data : [],
|
|
423
|
+
warnings: Array.isArray(resp.warnings)
|
|
424
|
+
? resp.warnings.filter((warning) => typeof warning === "string")
|
|
372
425
|
: undefined,
|
|
373
|
-
}
|
|
426
|
+
}),
|
|
427
|
+
inProcess: () => localStore((deps) => __awaiter(this, void 0, void 0, function* () {
|
|
428
|
+
const searcher = new searcher_1.Searcher(deps.vectorDb);
|
|
429
|
+
return searcher.search(args.query, args.limit, { rerank: args.rerank, seeds: args.seeds }, args.filters, args.pathPrefix);
|
|
430
|
+
})),
|
|
431
|
+
daemonErrorMessage: (resp) => [resp.error, resp.hint]
|
|
432
|
+
.filter((value) => typeof value === "string" && value)
|
|
433
|
+
.join(": ") || "daemon search failed",
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* `graph.peek`: the symbol's single-hop graph plus its defining chunk's
|
|
438
|
+
* metadata, and the multi-hop caller tree when `depth > 1`. Shared by
|
|
439
|
+
* `peek_symbol` and `semantic_search`'s symbol mode, which used to issue the
|
|
440
|
+
* same `buildGraph` call through their own GraphBuilder.
|
|
441
|
+
*/
|
|
442
|
+
function peekSymbolGraph(root, symbol, depth) {
|
|
443
|
+
const scope = projectScope(root);
|
|
444
|
+
return graphVerb("graph.peek", {
|
|
445
|
+
projectRoot: root,
|
|
446
|
+
scope,
|
|
447
|
+
payload: { target: symbol, depth, includeTests: false },
|
|
448
|
+
render: (resp) => resp.peek,
|
|
449
|
+
inProcess: () => localStore((deps) => (0, graph_handler_1.runGraphPeek)(deps.vectorDb, {
|
|
450
|
+
symbol,
|
|
451
|
+
depth,
|
|
452
|
+
scope,
|
|
453
|
+
includeTests: false,
|
|
454
|
+
})),
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
/**
|
|
458
|
+
* `graph.resolve`: a target (symbol name or file path) to the symbols it
|
|
459
|
+
* names. Shared by `find_tests` and `impact_analysis`.
|
|
460
|
+
*/
|
|
461
|
+
function resolveTarget(root, target) {
|
|
462
|
+
const scope = projectScope(root);
|
|
463
|
+
return graphVerb("graph.resolve", {
|
|
464
|
+
projectRoot: root,
|
|
465
|
+
scope,
|
|
466
|
+
payload: { target },
|
|
467
|
+
render: (resp) => {
|
|
468
|
+
var _a, _b;
|
|
469
|
+
return ({
|
|
470
|
+
symbols: (_a = resp.symbols) !== null && _a !== void 0 ? _a : [],
|
|
471
|
+
resolvedAsFile: resp.resolvedAsFile === true,
|
|
472
|
+
symbolFamilies: (_b = resp.symbolFamilies) !== null && _b !== void 0 ? _b : null,
|
|
473
|
+
});
|
|
474
|
+
},
|
|
475
|
+
inProcess: () => localStore((deps) => (0, graph_handler_1.runGraphResolve)(deps.vectorDb, { target, projectRoot: root })),
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
/** `graph.tests`: the reverse call graph's test hits for a symbol set. */
|
|
479
|
+
function findTestsForSymbols(root, symbols, depth, families) {
|
|
480
|
+
const scope = projectScope(root);
|
|
481
|
+
return graphVerb("graph.tests", {
|
|
482
|
+
projectRoot: root,
|
|
483
|
+
scope,
|
|
484
|
+
payload: { symbols, depth, families },
|
|
485
|
+
render: (resp) => { var _a; return (_a = resp.hits) !== null && _a !== void 0 ? _a : []; },
|
|
486
|
+
inProcess: () => localStore((deps) => (0, graph_handler_1.runGraphTests)(deps.vectorDb, {
|
|
487
|
+
symbols,
|
|
488
|
+
queryRoot: root,
|
|
489
|
+
depth,
|
|
490
|
+
excludePrefixes: scope.excludePrefixes,
|
|
491
|
+
families: (0, graph_handler_1.decodeSymbolFamilies)(families),
|
|
492
|
+
})),
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
/**
|
|
496
|
+
* `rows.locate`: a batch of row selects, one result array per matcher in
|
|
497
|
+
* order. Batched so a per-symbol loop costs one round trip, not hundreds.
|
|
498
|
+
*/
|
|
499
|
+
function locateRows(root, req) {
|
|
500
|
+
return (0, rows_handler_1.readRows)(Object.assign({ name: "locate", projectRoot: root, lancedbDir: paths.lancedbDir, scope: projectScope(root), fallback: MCP_STORE_FALLBACK }, req));
|
|
501
|
+
}
|
|
502
|
+
/**
|
|
503
|
+
* Chunk and file totals for `index_status`, summed per registered project.
|
|
504
|
+
*
|
|
505
|
+
* The old code asked the store for `countRows()` and a distinct-path scan
|
|
506
|
+
* over the whole table. There is no whole-table read verb — deliberately:
|
|
507
|
+
* the daemon's `project-stats` is per project, and summing it keeps the
|
|
508
|
+
* daemon and in-process paths on the *same* per-prefix counts instead of
|
|
509
|
+
* one counting orphaned rows the other cannot see. A failed per-project
|
|
510
|
+
* call contributes nothing rather than failing the tool, exactly as
|
|
511
|
+
* `gmax status` degrades to its cached counts.
|
|
512
|
+
*/
|
|
513
|
+
function indexTotals(projects) {
|
|
514
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
515
|
+
let chunks = 0;
|
|
516
|
+
let files = 0;
|
|
517
|
+
for (const project of projects) {
|
|
518
|
+
const totals = yield readStore("project-stats", {
|
|
519
|
+
daemon: () => (0, daemon_client_1.sendDaemonCommand)({ cmd: "project-stats", root: project.root }, { timeoutMs: 30000 }),
|
|
520
|
+
render: (resp) => ({
|
|
521
|
+
chunks: typeof resp.chunks === "number" ? resp.chunks : 0,
|
|
522
|
+
files: typeof resp.files === "number" ? resp.files : 0,
|
|
523
|
+
}),
|
|
524
|
+
inProcess: () => localStore((deps) => __awaiter(this, void 0, void 0, function* () {
|
|
525
|
+
const prefix = project.root.endsWith("/")
|
|
526
|
+
? project.root
|
|
527
|
+
: `${project.root}/`;
|
|
528
|
+
return {
|
|
529
|
+
chunks: yield deps.vectorDb.countRowsForPath(prefix),
|
|
530
|
+
files: yield deps.vectorDb.countDistinctFilesForPath(prefix),
|
|
531
|
+
};
|
|
532
|
+
})),
|
|
533
|
+
}).catch(() => ({ chunks: 0, files: 0 }));
|
|
534
|
+
chunks += totals.chunks;
|
|
535
|
+
files += totals.files;
|
|
536
|
+
}
|
|
537
|
+
return { chunks, files };
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
/** `rows.skeleton`: the stored per-file skeleton (and the symbol lookup). */
|
|
541
|
+
function lookupSkeleton(root, req) {
|
|
542
|
+
return readStore("skeleton", {
|
|
543
|
+
daemon: () => (0, daemon_client_1.sendDaemonCommand)(Object.assign({ cmd: "rows.skeleton", projectRoot: root }, req), { timeoutMs: 30000 }),
|
|
544
|
+
render: (resp) => {
|
|
545
|
+
var _a, _b;
|
|
546
|
+
return ({
|
|
547
|
+
path: ((_a = resp.path) !== null && _a !== void 0 ? _a : null),
|
|
548
|
+
skeleton: ((_b = resp.skeleton) !== null && _b !== void 0 ? _b : null),
|
|
549
|
+
});
|
|
550
|
+
},
|
|
551
|
+
inProcess: () => localStore((deps) => (0, rows_handler_1.runSkeleton)(deps, Object.assign({ projectRoot: root }, req))),
|
|
374
552
|
});
|
|
375
553
|
}
|
|
376
554
|
function getSkeletonizer() {
|
|
@@ -428,7 +606,7 @@ exports.mcp = new commander_1.Command("mcp")
|
|
|
428
606
|
// --- Tool handlers ---
|
|
429
607
|
function handleSemanticSearch(args_1) {
|
|
430
608
|
return __awaiter(this, arguments, void 0, function* (args, isSearchAll = false) {
|
|
431
|
-
var _a, _b
|
|
609
|
+
var _a, _b;
|
|
432
610
|
const query = String(args.query || "");
|
|
433
611
|
if (!query)
|
|
434
612
|
return err("Missing required parameter: query");
|
|
@@ -538,10 +716,10 @@ exports.mcp = new commander_1.Command("mcp")
|
|
|
538
716
|
rerank: process.env.GMAX_RERANK === "1",
|
|
539
717
|
seeds,
|
|
540
718
|
};
|
|
541
|
-
const result =
|
|
542
|
-
limit, filters: effectiveFilters, pathPrefix }, searchOptions))
|
|
719
|
+
const result = yield searchStore(Object.assign({ projectRoot: resolvedRoot, query,
|
|
720
|
+
limit, filters: effectiveFilters, pathPrefix }, searchOptions));
|
|
543
721
|
const allowedRoots = searchAll
|
|
544
|
-
? ((
|
|
722
|
+
? ((_b = filters.projectRoots) !== null && _b !== void 0 ? _b : [])
|
|
545
723
|
: [resolvedRoot];
|
|
546
724
|
result.data = result.data.filter((chunk) => {
|
|
547
725
|
const candidate = chunkAbsPath(chunk);
|
|
@@ -684,7 +862,7 @@ exports.mcp = new commander_1.Command("mcp")
|
|
|
684
862
|
const regex = new RegExp(namePattern, "i");
|
|
685
863
|
results = results.filter((r) => r.symbols.some((s) => regex.test(s)));
|
|
686
864
|
}
|
|
687
|
-
catch (
|
|
865
|
+
catch (_c) {
|
|
688
866
|
// Invalid regex — skip filter
|
|
689
867
|
}
|
|
690
868
|
}
|
|
@@ -692,9 +870,10 @@ exports.mcp = new commander_1.Command("mcp")
|
|
|
692
870
|
// Symbol mode: append call graph
|
|
693
871
|
if (mode === "symbol" && !searchAll) {
|
|
694
872
|
try {
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
873
|
+
// `graph.peek` is the single-hop `buildGraph` this used to call,
|
|
874
|
+
// plus metadata this branch ignores — one verb rather than a second
|
|
875
|
+
// shape for the same query.
|
|
876
|
+
const { graph } = yield peekSymbolGraph(resolvedRoot, query, 1);
|
|
698
877
|
if (graph.center) {
|
|
699
878
|
const traceLines = ["", "--- Call graph ---"];
|
|
700
879
|
const centerRel = graph.center.file.startsWith(resolvedRoot)
|
|
@@ -727,15 +906,14 @@ exports.mcp = new commander_1.Command("mcp")
|
|
|
727
906
|
output += `\n${traceLines.join("\n")}`;
|
|
728
907
|
}
|
|
729
908
|
}
|
|
730
|
-
catch (
|
|
909
|
+
catch (_d) {
|
|
731
910
|
// Trace failed — return search results without trace
|
|
732
911
|
}
|
|
733
912
|
}
|
|
734
913
|
return ok(prefixNotes(output));
|
|
735
914
|
}
|
|
736
915
|
catch (e) {
|
|
737
|
-
|
|
738
|
-
return err(`Search failed: ${msg}`);
|
|
916
|
+
return toolError("Search failed", e);
|
|
739
917
|
}
|
|
740
918
|
});
|
|
741
919
|
}
|
|
@@ -800,10 +978,10 @@ exports.mcp = new commander_1.Command("mcp")
|
|
|
800
978
|
let skeleton = "";
|
|
801
979
|
let language = "";
|
|
802
980
|
let tokenEstimate = 0;
|
|
803
|
-
// Try
|
|
981
|
+
// Try the stored skeleton first, via the daemon's warm store.
|
|
804
982
|
try {
|
|
805
|
-
const
|
|
806
|
-
|
|
983
|
+
const cached = (yield lookupSkeleton(root, { path: absPath }))
|
|
984
|
+
.skeleton;
|
|
807
985
|
if (cached) {
|
|
808
986
|
skeleton = cached;
|
|
809
987
|
tokenEstimate = Math.ceil(cached.length / 4);
|
|
@@ -871,10 +1049,15 @@ exports.mcp = new commander_1.Command("mcp")
|
|
|
871
1049
|
}
|
|
872
1050
|
void ensureWatcher(root);
|
|
873
1051
|
try {
|
|
874
|
-
const db = getVectorDb();
|
|
875
|
-
const builder = new graph_builder_1.GraphBuilder(db, root);
|
|
876
1052
|
const depth = Math.min(Math.max(Number(args.depth) || 1, 1), 3);
|
|
877
|
-
const
|
|
1053
|
+
const scope = projectScope(root);
|
|
1054
|
+
const graph = yield graphVerb("graph.trace", {
|
|
1055
|
+
projectRoot: root,
|
|
1056
|
+
scope,
|
|
1057
|
+
payload: { target: symbol, hops: depth },
|
|
1058
|
+
render: (resp) => resp.graph,
|
|
1059
|
+
inProcess: () => localStore((deps) => (0, graph_handler_1.runGraphTrace)(deps.vectorDb, { symbol, hops: depth, scope })),
|
|
1060
|
+
});
|
|
878
1061
|
if (!graph.center) {
|
|
879
1062
|
return ok(`Symbol '${symbol}' not found in the index. Check \`gmax status\` to see which projects are indexed, or try \`gmax search ${symbol}\` to find similar symbols.`);
|
|
880
1063
|
}
|
|
@@ -938,8 +1121,7 @@ exports.mcp = new commander_1.Command("mcp")
|
|
|
938
1121
|
return ok(lines.join("\n"));
|
|
939
1122
|
}
|
|
940
1123
|
catch (e) {
|
|
941
|
-
|
|
942
|
-
return err(`Trace failed: ${msg}`);
|
|
1124
|
+
return toolError("Trace failed", e);
|
|
943
1125
|
}
|
|
944
1126
|
});
|
|
945
1127
|
}
|
|
@@ -953,23 +1135,21 @@ exports.mcp = new commander_1.Command("mcp")
|
|
|
953
1135
|
const { proj, root } = resolveRegisteredProject(args.root);
|
|
954
1136
|
if (!proj)
|
|
955
1137
|
return err("Project not added to gmax yet.");
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
const
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
"
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
.toArray();
|
|
972
|
-
if (rows.length === 0) {
|
|
1138
|
+
// Locations come from the daemon; the body is read below, from this
|
|
1139
|
+
// process's own filesystem view — same split the `extract` CLI makes.
|
|
1140
|
+
const [rows] = yield locateRows(root, {
|
|
1141
|
+
select: [
|
|
1142
|
+
"path",
|
|
1143
|
+
"start_line",
|
|
1144
|
+
"end_line",
|
|
1145
|
+
"role",
|
|
1146
|
+
"is_exported",
|
|
1147
|
+
"defined_symbols",
|
|
1148
|
+
],
|
|
1149
|
+
matches: [{ kind: "definedSymbol", symbol }],
|
|
1150
|
+
limit: 10,
|
|
1151
|
+
});
|
|
1152
|
+
if (!rows || rows.length === 0) {
|
|
973
1153
|
return ok(`Symbol '${symbol}' not found in the index. Check \`gmax status\` to see which projects are indexed, or try \`gmax search ${symbol}\` to find similar symbols.`);
|
|
974
1154
|
}
|
|
975
1155
|
// Pick best match: prefer exact first-defined, then highest role
|
|
@@ -978,7 +1158,7 @@ exports.mcp = new commander_1.Command("mcp")
|
|
|
978
1158
|
DEFINITION: 2,
|
|
979
1159
|
IMPLEMENTATION: 1,
|
|
980
1160
|
};
|
|
981
|
-
const sorted = rows.sort((a, b) => {
|
|
1161
|
+
const sorted = [...rows].sort((a, b) => {
|
|
982
1162
|
const aDefs = Array.isArray(a.defined_symbols)
|
|
983
1163
|
? a.defined_symbols
|
|
984
1164
|
: [];
|
|
@@ -1031,13 +1211,13 @@ exports.mcp = new commander_1.Command("mcp")
|
|
|
1031
1211
|
return ok(parts.join("\n"));
|
|
1032
1212
|
}
|
|
1033
1213
|
catch (e) {
|
|
1034
|
-
|
|
1035
|
-
return err(`Extract failed: ${msg}`);
|
|
1214
|
+
return toolError("Extract failed", e);
|
|
1036
1215
|
}
|
|
1037
1216
|
});
|
|
1038
1217
|
}
|
|
1039
1218
|
function handlePeekSymbol(args) {
|
|
1040
1219
|
return __awaiter(this, void 0, void 0, function* () {
|
|
1220
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
1041
1221
|
ensureWatcher();
|
|
1042
1222
|
const symbol = String(args.symbol || "");
|
|
1043
1223
|
if (!symbol)
|
|
@@ -1047,31 +1227,21 @@ exports.mcp = new commander_1.Command("mcp")
|
|
|
1047
1227
|
if (!proj)
|
|
1048
1228
|
return err("Project not added to gmax yet.");
|
|
1049
1229
|
const depth = Math.min(Math.max(Number(args.depth || 1), 1), 3);
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
const
|
|
1230
|
+
// One verb for the whole tool: the graph, the defining chunk's
|
|
1231
|
+
// is_exported/start_line/end_line, and (when depth > 1) the multi-hop
|
|
1232
|
+
// caller tree. Four queries became one round trip.
|
|
1233
|
+
const peek = yield peekSymbolGraph(root, symbol, depth);
|
|
1234
|
+
const graph = peek.graph;
|
|
1054
1235
|
if (!graph.center) {
|
|
1055
1236
|
return ok(`Symbol '${symbol}' not found in the index. Check \`gmax status\` to see which projects are indexed, or try \`gmax search ${symbol}\` to find similar symbols.`);
|
|
1056
1237
|
}
|
|
1057
1238
|
const center = graph.center;
|
|
1058
1239
|
const rel = (p) => p.startsWith(root) ? p.slice(root.length + 1) : p;
|
|
1059
|
-
//
|
|
1060
|
-
|
|
1061
|
-
const
|
|
1062
|
-
const
|
|
1063
|
-
|
|
1064
|
-
.select(["is_exported", "start_line", "end_line"])
|
|
1065
|
-
.where(`array_contains(defined_symbols, '${(0, filter_builder_1.escapeSqlString)(symbol)}') AND ${(0, filter_builder_1.pathStartsWith)(prefix)}`)
|
|
1066
|
-
.limit(1)
|
|
1067
|
-
.toArray();
|
|
1068
|
-
const exported = metaRows.length > 0 && Boolean(metaRows[0].is_exported);
|
|
1069
|
-
const startLine = metaRows.length > 0
|
|
1070
|
-
? Number(metaRows[0].start_line || 0)
|
|
1071
|
-
: center.line;
|
|
1072
|
-
const endLine = metaRows.length > 0
|
|
1073
|
-
? Number(metaRows[0].end_line || 0)
|
|
1074
|
-
: center.line;
|
|
1240
|
+
// Chunk metadata for is_exported and end_line; null when the symbol has
|
|
1241
|
+
// no defining chunk, in which case the center's own line stands in.
|
|
1242
|
+
const exported = (_b = (_a = peek.meta) === null || _a === void 0 ? void 0 : _a.isExported) !== null && _b !== void 0 ? _b : false;
|
|
1243
|
+
const startLine = (_d = (_c = peek.meta) === null || _c === void 0 ? void 0 : _c.startLine) !== null && _d !== void 0 ? _d : center.line;
|
|
1244
|
+
const endLine = (_f = (_e = peek.meta) === null || _e === void 0 ? void 0 : _e.endLine) !== null && _f !== void 0 ? _f : center.line;
|
|
1075
1245
|
// Get signature from source
|
|
1076
1246
|
const fs = yield Promise.resolve().then(() => __importStar(require("node:fs")));
|
|
1077
1247
|
let sigText = "(source not available)";
|
|
@@ -1090,11 +1260,10 @@ exports.mcp = new commander_1.Command("mcp")
|
|
|
1090
1260
|
}
|
|
1091
1261
|
sigText = sigLines.join("\n").trim();
|
|
1092
1262
|
}
|
|
1093
|
-
catch (
|
|
1263
|
+
catch (_h) { }
|
|
1094
1264
|
// Get callers
|
|
1095
1265
|
let callerList;
|
|
1096
1266
|
if (depth > 1) {
|
|
1097
|
-
const multiHop = yield builder.buildGraphMultiHop(symbol, depth);
|
|
1098
1267
|
const flat = [];
|
|
1099
1268
|
function walkCallers(tree) {
|
|
1100
1269
|
for (const t of tree) {
|
|
@@ -1107,7 +1276,7 @@ exports.mcp = new commander_1.Command("mcp")
|
|
|
1107
1276
|
walkCallers(t.callers);
|
|
1108
1277
|
}
|
|
1109
1278
|
}
|
|
1110
|
-
walkCallers(
|
|
1279
|
+
walkCallers((_g = peek.callerTree) !== null && _g !== void 0 ? _g : []);
|
|
1111
1280
|
callerList = flat;
|
|
1112
1281
|
}
|
|
1113
1282
|
else {
|
|
@@ -1165,8 +1334,7 @@ exports.mcp = new commander_1.Command("mcp")
|
|
|
1165
1334
|
return ok(parts.join("\n"));
|
|
1166
1335
|
}
|
|
1167
1336
|
catch (e) {
|
|
1168
|
-
|
|
1169
|
-
return err(`Peek failed: ${msg}`);
|
|
1337
|
+
return toolError("Peek failed", e);
|
|
1170
1338
|
}
|
|
1171
1339
|
});
|
|
1172
1340
|
}
|
|
@@ -1180,35 +1348,28 @@ exports.mcp = new commander_1.Command("mcp")
|
|
|
1180
1348
|
const { proj, root } = resolveRegisteredProject(args.root);
|
|
1181
1349
|
if (!proj)
|
|
1182
1350
|
return err("Project not added to gmax yet.");
|
|
1183
|
-
const
|
|
1184
|
-
const
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
if (defRows.length === 0) {
|
|
1351
|
+
const scope = projectScope(root);
|
|
1352
|
+
const facts = yield graphVerb("graph.dead", {
|
|
1353
|
+
projectRoot: root,
|
|
1354
|
+
scope,
|
|
1355
|
+
payload: { target: symbol },
|
|
1356
|
+
render: (resp) => resp.dead,
|
|
1357
|
+
inProcess: () => localStore((deps) => (0, graph_handler_1.runGraphDead)(deps.vectorDb, { symbol, scope })),
|
|
1358
|
+
});
|
|
1359
|
+
if (!facts.found) {
|
|
1193
1360
|
return ok(`Symbol '${symbol}' not found in the index. Check \`gmax status\` to see which projects are indexed, or try \`gmax search ${symbol}\` to find similar symbols.`);
|
|
1194
1361
|
}
|
|
1195
|
-
const defRow = defRows[0];
|
|
1196
|
-
const defPath = String(defRow.path || "");
|
|
1197
|
-
const defLine = Number(defRow.start_line || 0);
|
|
1198
|
-
const isExported = Boolean(defRow.is_exported);
|
|
1199
|
-
const builder = new graph_builder_1.GraphBuilder(db, root);
|
|
1200
|
-
const callers = yield builder.getCallers(symbol, (0, languages_1.languageFamilyForPath)(defPath));
|
|
1201
1362
|
const rel = (p) => p.startsWith(root) ? p.slice(root.length + 1) : p;
|
|
1202
|
-
const defLoc = `${rel(defPath)}:${defLine + 1}`;
|
|
1203
|
-
if (
|
|
1204
|
-
if (isExported) {
|
|
1363
|
+
const defLoc = `${rel(facts.defPath)}:${facts.defLine + 1}`;
|
|
1364
|
+
if (facts.callerCount === 0) {
|
|
1365
|
+
if (facts.isExported) {
|
|
1205
1366
|
return ok(`PUBLIC EXPORT ${defLoc} defines ${symbol} — no internal callers found; check external usage`);
|
|
1206
1367
|
}
|
|
1207
1368
|
return ok(`DEAD ${defLoc} defines ${symbol}`);
|
|
1208
1369
|
}
|
|
1209
|
-
const top =
|
|
1370
|
+
const top = facts.topCallers;
|
|
1210
1371
|
const lines = [
|
|
1211
|
-
`LIVE ${defLoc} defines ${symbol} — ${
|
|
1372
|
+
`LIVE ${defLoc} defines ${symbol} — ${facts.callerCount} inbound caller${facts.callerCount === 1 ? "" : "s"} (top ${top.length}):`,
|
|
1212
1373
|
];
|
|
1213
1374
|
for (const c of top) {
|
|
1214
1375
|
lines.push(` ${rel(c.file)}:${c.line + 1}`);
|
|
@@ -1216,8 +1377,7 @@ exports.mcp = new commander_1.Command("mcp")
|
|
|
1216
1377
|
return ok(lines.join("\n"));
|
|
1217
1378
|
}
|
|
1218
1379
|
catch (e) {
|
|
1219
|
-
|
|
1220
|
-
return err(`Dead check failed: ${msg}`);
|
|
1380
|
+
return toolError("Dead check failed", e);
|
|
1221
1381
|
}
|
|
1222
1382
|
});
|
|
1223
1383
|
}
|
|
@@ -1227,39 +1387,25 @@ exports.mcp = new commander_1.Command("mcp")
|
|
|
1227
1387
|
const { proj, root } = resolveRegisteredProject(args.root);
|
|
1228
1388
|
if (!proj)
|
|
1229
1389
|
return err("Project not added to gmax yet.");
|
|
1230
|
-
const prefix = root.endsWith("/") ? root : `${root}/`;
|
|
1231
1390
|
const top = Math.min(Math.max(Number(args.top) || 10, 1), 50);
|
|
1232
1391
|
try {
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
const
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
if (rows.length === 0) {
|
|
1392
|
+
// The 500k-row read and its aggregation stay wherever the store is
|
|
1393
|
+
// open; what crosses the socket is the finished report.
|
|
1394
|
+
const scope = projectScope(root);
|
|
1395
|
+
const audit = yield graphVerb("graph.audit", {
|
|
1396
|
+
projectRoot: root,
|
|
1397
|
+
scope,
|
|
1398
|
+
payload: { top },
|
|
1399
|
+
render: (resp) => { var _a; return (_a = resp.audit) !== null && _a !== void 0 ? _a : null; },
|
|
1400
|
+
inProcess: () => localStore((deps) => (0, graph_handler_1.runGraphAudit)(deps.vectorDb, {
|
|
1401
|
+
projectRoot: root,
|
|
1402
|
+
scope,
|
|
1403
|
+
top,
|
|
1404
|
+
})),
|
|
1405
|
+
});
|
|
1406
|
+
if (!audit) {
|
|
1249
1407
|
return ok(`No indexed data found for ${root}. Run: gmax index --path ${root}`);
|
|
1250
1408
|
}
|
|
1251
|
-
const audit = (0, audit_1.computeAudit)(rows.map((r) => ({
|
|
1252
|
-
path: String(r.path || ""),
|
|
1253
|
-
start_line: Number(r.start_line || 0),
|
|
1254
|
-
is_exported: Boolean(r.is_exported),
|
|
1255
|
-
defined_symbols: toStringArray(r.defined_symbols),
|
|
1256
|
-
referenced_symbols: [
|
|
1257
|
-
...new Set([
|
|
1258
|
-
...toStringArray(r.referenced_symbols),
|
|
1259
|
-
...toStringArray(r.type_referenced_symbols),
|
|
1260
|
-
]),
|
|
1261
|
-
],
|
|
1262
|
-
})), prefix, top);
|
|
1263
1409
|
const lines = [];
|
|
1264
1410
|
lines.push(`Audit — ${audit.scannedChunks} chunks across ${audit.scannedFiles} files`);
|
|
1265
1411
|
lines.push("");
|
|
@@ -1298,8 +1444,7 @@ exports.mcp = new commander_1.Command("mcp")
|
|
|
1298
1444
|
return ok(lines.join("\n"));
|
|
1299
1445
|
}
|
|
1300
1446
|
catch (e) {
|
|
1301
|
-
|
|
1302
|
-
return err(`Audit failed: ${msg}`);
|
|
1447
|
+
return toolError("Audit failed", e);
|
|
1303
1448
|
}
|
|
1304
1449
|
});
|
|
1305
1450
|
}
|
|
@@ -1318,25 +1463,35 @@ exports.mcp = new commander_1.Command("mcp")
|
|
|
1318
1463
|
const dirDepth = Math.min(Math.max(Number(args.dir_depth) || surprising_connections_1.DEFAULT_SURPRISE_OPTIONS.dirDepth, 1), 20);
|
|
1319
1464
|
const minSimilarity = Math.max(Number(args.min_similarity) || surprising_connections_1.DEFAULT_SURPRISE_OPTIONS.minSimilarity, 0);
|
|
1320
1465
|
const maxRows = Math.min(Math.max(Number(args.max_rows) || surprising_connections_1.DEFAULT_SURPRISE_OPTIONS.maxRows, 1), surprising_connections_1.MAX_SURPRISE_ROWS);
|
|
1466
|
+
const options = {
|
|
1467
|
+
sample,
|
|
1468
|
+
neighbors,
|
|
1469
|
+
dirDepth,
|
|
1470
|
+
minSimilarity,
|
|
1471
|
+
maxRows,
|
|
1472
|
+
includeTests: Boolean(args.include_tests),
|
|
1473
|
+
includeEval: Boolean(args.include_eval),
|
|
1474
|
+
in: typeof args.in === "string" ? [args.in] : undefined,
|
|
1475
|
+
exclude: typeof args.exclude === "string" ? [args.exclude] : undefined,
|
|
1476
|
+
};
|
|
1321
1477
|
try {
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1478
|
+
// The scan can touch 50k rows and every one of their vectors; the verb
|
|
1479
|
+
// returns the summary plus the findings this will print, and drops the
|
|
1480
|
+
// raw `pairs` array entirely.
|
|
1481
|
+
const result = yield readStore("surprises", {
|
|
1482
|
+
daemon: () => (0, daemon_client_1.sendDaemonCommand)({
|
|
1483
|
+
cmd: "vector.surprises",
|
|
1484
|
+
projectRoot: root,
|
|
1485
|
+
options,
|
|
1486
|
+
top,
|
|
1487
|
+
}, { timeoutMs: 120000 }),
|
|
1488
|
+
render: (resp) => resp,
|
|
1489
|
+
inProcess: () => localStore((deps) => (0, vector_handler_1.runSurprises)(deps, { projectRoot: root, options, top })),
|
|
1334
1490
|
});
|
|
1335
1491
|
return ok(formatMcpSurprisingConnections(result, top));
|
|
1336
1492
|
}
|
|
1337
1493
|
catch (e) {
|
|
1338
|
-
|
|
1339
|
-
return err(`Surprising connections failed: ${msg}`);
|
|
1494
|
+
return toolError("Surprising connections failed", e);
|
|
1340
1495
|
}
|
|
1341
1496
|
});
|
|
1342
1497
|
}
|
|
@@ -1352,8 +1507,19 @@ exports.mcp = new commander_1.Command("mcp")
|
|
|
1352
1507
|
const { proj, root } = resolveRegisteredProject(args.root);
|
|
1353
1508
|
if (!proj)
|
|
1354
1509
|
return err("Project not added to gmax yet.");
|
|
1355
|
-
const
|
|
1356
|
-
const hits = yield
|
|
1510
|
+
const scope = projectScope(root);
|
|
1511
|
+
const hits = yield graphVerb("graph.neighbors", {
|
|
1512
|
+
projectRoot: root,
|
|
1513
|
+
scope,
|
|
1514
|
+
payload: { symbol, direction, maxHops },
|
|
1515
|
+
render: (resp) => { var _a; return (_a = resp.hits) !== null && _a !== void 0 ? _a : []; },
|
|
1516
|
+
inProcess: () => localStore((deps) => (0, graph_handler_1.runGraphNeighbors)(deps.vectorDb, {
|
|
1517
|
+
symbol,
|
|
1518
|
+
direction,
|
|
1519
|
+
maxHops,
|
|
1520
|
+
scope,
|
|
1521
|
+
})),
|
|
1522
|
+
});
|
|
1357
1523
|
if (hits.length === 0) {
|
|
1358
1524
|
return ok(`No ${direction} found for '${symbol}' within ${maxHops} hop(s). Check it is indexed (\`gmax status\`) or try the \`dead\` tool.`);
|
|
1359
1525
|
}
|
|
@@ -1370,8 +1536,7 @@ exports.mcp = new commander_1.Command("mcp")
|
|
|
1370
1536
|
return ok(lines.join("\n"));
|
|
1371
1537
|
}
|
|
1372
1538
|
catch (e) {
|
|
1373
|
-
|
|
1374
|
-
return err(`get_neighbors failed: ${msg}`);
|
|
1539
|
+
return toolError("get_neighbors failed", e);
|
|
1375
1540
|
}
|
|
1376
1541
|
});
|
|
1377
1542
|
}
|
|
@@ -1389,8 +1554,20 @@ exports.mcp = new commander_1.Command("mcp")
|
|
|
1389
1554
|
const { proj, root } = resolveRegisteredProject(args.root);
|
|
1390
1555
|
if (!proj)
|
|
1391
1556
|
return err("Project not added to gmax yet.");
|
|
1392
|
-
const
|
|
1393
|
-
const pathSyms = yield
|
|
1557
|
+
const scope = projectScope(root);
|
|
1558
|
+
const pathSyms = yield graphVerb("graph.paths", {
|
|
1559
|
+
projectRoot: root,
|
|
1560
|
+
scope,
|
|
1561
|
+
payload: { from, to, direction, maxHops },
|
|
1562
|
+
render: (resp) => { var _a; return (_a = resp.path) !== null && _a !== void 0 ? _a : null; },
|
|
1563
|
+
inProcess: () => localStore((deps) => (0, graph_handler_1.runGraphPaths)(deps.vectorDb, {
|
|
1564
|
+
from,
|
|
1565
|
+
to,
|
|
1566
|
+
direction,
|
|
1567
|
+
maxHops,
|
|
1568
|
+
scope,
|
|
1569
|
+
})),
|
|
1570
|
+
});
|
|
1394
1571
|
if (!pathSyms) {
|
|
1395
1572
|
return ok(`No ${direction} path from '${from}' to '${to}' within ${maxHops} hops.`);
|
|
1396
1573
|
}
|
|
@@ -1399,8 +1576,7 @@ exports.mcp = new commander_1.Command("mcp")
|
|
|
1399
1576
|
return ok(`Path (${hops} hop${hops === 1 ? "" : "s"}): ${pathSyms.join(arrow)}`);
|
|
1400
1577
|
}
|
|
1401
1578
|
catch (e) {
|
|
1402
|
-
|
|
1403
|
-
return err(`find_paths failed: ${msg}`);
|
|
1579
|
+
return toolError("find_paths failed", e);
|
|
1404
1580
|
}
|
|
1405
1581
|
});
|
|
1406
1582
|
}
|
|
@@ -1417,9 +1593,18 @@ exports.mcp = new commander_1.Command("mcp")
|
|
|
1417
1593
|
const { proj, root } = resolveRegisteredProject(args.root);
|
|
1418
1594
|
if (!proj)
|
|
1419
1595
|
return err("Project not added to gmax yet.");
|
|
1596
|
+
// Resolved here, not daemon-side: verifyExistingTarget looks at this
|
|
1597
|
+
// process's filesystem view, and a path outside the project must fail
|
|
1598
|
+
// with the containment error before anything reaches the store.
|
|
1420
1599
|
const abs = filesIn.map((f) => (0, path_containment_1.resolveContainedPath)(root, f, { verifyExistingTarget: true }));
|
|
1421
|
-
const
|
|
1422
|
-
const sg = yield
|
|
1600
|
+
const scope = projectScope(root);
|
|
1601
|
+
const sg = yield graphVerb("graph.subgraph", {
|
|
1602
|
+
projectRoot: root,
|
|
1603
|
+
scope,
|
|
1604
|
+
payload: { files: abs },
|
|
1605
|
+
render: (resp) => resp.subgraph,
|
|
1606
|
+
inProcess: () => localStore((deps) => (0, graph_handler_1.runGraphSubgraph)(deps.vectorDb, { files: abs, scope })),
|
|
1607
|
+
});
|
|
1423
1608
|
if (sg.symbols.length === 0) {
|
|
1424
1609
|
return ok(`No indexed symbols found in: ${filesIn.join(", ")}. Check the paths and \`gmax status\`.`);
|
|
1425
1610
|
}
|
|
@@ -1447,8 +1632,7 @@ exports.mcp = new commander_1.Command("mcp")
|
|
|
1447
1632
|
return ok(lines.join("\n"));
|
|
1448
1633
|
}
|
|
1449
1634
|
catch (e) {
|
|
1450
|
-
|
|
1451
|
-
return err(`subgraph_for_files failed: ${msg}`);
|
|
1635
|
+
return toolError("subgraph_for_files failed", e);
|
|
1452
1636
|
}
|
|
1453
1637
|
});
|
|
1454
1638
|
}
|
|
@@ -1463,60 +1647,29 @@ exports.mcp = new commander_1.Command("mcp")
|
|
|
1463
1647
|
return err("Project not added to gmax yet. Run `gmax add` to index it first.");
|
|
1464
1648
|
}
|
|
1465
1649
|
try {
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
const
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
const rowPath = String(row.path || "");
|
|
1490
|
-
const line = Number(row.start_line || 0);
|
|
1491
|
-
const role = String(row.role || "");
|
|
1492
|
-
const exported = Boolean(row.is_exported);
|
|
1493
|
-
for (const sym of defs) {
|
|
1494
|
-
if (pattern && !sym.toLowerCase().includes(pattern.toLowerCase())) {
|
|
1495
|
-
continue;
|
|
1496
|
-
}
|
|
1497
|
-
const existing = map.get(sym);
|
|
1498
|
-
if (existing) {
|
|
1499
|
-
existing.count += 1;
|
|
1500
|
-
}
|
|
1501
|
-
else {
|
|
1502
|
-
map.set(sym, {
|
|
1503
|
-
symbol: sym,
|
|
1504
|
-
count: 1,
|
|
1505
|
-
path: rowPath,
|
|
1506
|
-
line: Math.max(1, line + 1),
|
|
1507
|
-
role,
|
|
1508
|
-
exported,
|
|
1509
|
-
});
|
|
1510
|
-
}
|
|
1511
|
-
}
|
|
1512
|
-
}
|
|
1513
|
-
const entries = Array.from(map.values())
|
|
1514
|
-
.sort((a, b) => {
|
|
1515
|
-
if (b.count !== a.count)
|
|
1516
|
-
return b.count - a.count;
|
|
1517
|
-
return a.symbol.localeCompare(b.symbol);
|
|
1518
|
-
})
|
|
1519
|
-
.slice(0, limit);
|
|
1650
|
+
// A single absolute prefix carries both scopes: the sub-path when one
|
|
1651
|
+
// was asked for (it is already inside the root), the root otherwise.
|
|
1652
|
+
const absPrefix = pathPrefix
|
|
1653
|
+
? (0, path_containment_1.resolveContainedPath)(root, pathPrefix)
|
|
1654
|
+
: root.endsWith("/")
|
|
1655
|
+
? root
|
|
1656
|
+
: `${root}/`;
|
|
1657
|
+
const entries = yield readStore("symbols", {
|
|
1658
|
+
daemon: () => (0, daemon_client_1.sendDaemonCommand)({
|
|
1659
|
+
cmd: "rows.symbols",
|
|
1660
|
+
projectRoot: root,
|
|
1661
|
+
pathPrefix: absPrefix,
|
|
1662
|
+
pattern,
|
|
1663
|
+
limit,
|
|
1664
|
+
}, { timeoutMs: 60000 }),
|
|
1665
|
+
render: (resp) => { var _a; return ((_a = resp.entries) !== null && _a !== void 0 ? _a : []); },
|
|
1666
|
+
inProcess: () => localStore((deps) => (0, rows_handler_1.runSymbols)(deps, {
|
|
1667
|
+
projectRoot: root,
|
|
1668
|
+
pathPrefix: absPrefix,
|
|
1669
|
+
pattern,
|
|
1670
|
+
limit,
|
|
1671
|
+
})),
|
|
1672
|
+
});
|
|
1520
1673
|
if (entries.length === 0) {
|
|
1521
1674
|
return ok("No symbols found. Run `gmax status` to verify the project is indexed, or `gmax index` to rebuild.");
|
|
1522
1675
|
}
|
|
@@ -1526,13 +1679,14 @@ exports.mcp = new commander_1.Command("mcp")
|
|
|
1526
1679
|
: e.path;
|
|
1527
1680
|
const roleTag = e.role ? ` [${e.role.slice(0, 4)}]` : "";
|
|
1528
1681
|
const expTag = e.exported ? " exported" : "";
|
|
1529
|
-
|
|
1682
|
+
// SymbolEntry.line is the 0-based start_line the store holds; this
|
|
1683
|
+
// tool has always printed a 1-based line, floored at 1.
|
|
1684
|
+
return `${e.symbol}${roleTag}${expTag}\t${rel}:${Math.max(1, e.line + 1)}`;
|
|
1530
1685
|
});
|
|
1531
1686
|
return ok(lines.join("\n"));
|
|
1532
1687
|
}
|
|
1533
1688
|
catch (e) {
|
|
1534
|
-
|
|
1535
|
-
return err(`Symbol listing failed: ${msg}`);
|
|
1689
|
+
return toolError("Symbol listing failed", e);
|
|
1536
1690
|
}
|
|
1537
1691
|
});
|
|
1538
1692
|
}
|
|
@@ -1572,9 +1726,7 @@ exports.mcp = new commander_1.Command("mcp")
|
|
|
1572
1726
|
const projects = (0, project_registry_1.listProjects)();
|
|
1573
1727
|
const currentProject = resolveMcpProject(undefined, projectRoot);
|
|
1574
1728
|
const identity = (0, embedding_status_1.projectEmbeddingStatus)(currentProject, globalConfig);
|
|
1575
|
-
const
|
|
1576
|
-
const stats = yield db.getStats();
|
|
1577
|
-
const fileCount = yield db.getDistinctFileCount();
|
|
1729
|
+
const { chunks, files } = yield indexTotals(projects);
|
|
1578
1730
|
// Watcher status
|
|
1579
1731
|
const watcher = (0, watcher_store_1.getWatcherCoveringPath)(projectRoot);
|
|
1580
1732
|
let watcherLine = "Watcher: not running";
|
|
@@ -1593,7 +1745,7 @@ exports.mcp = new commander_1.Command("mcp")
|
|
|
1593
1745
|
// per-project chunk counts) lives in `list_projects` — keep this tool
|
|
1594
1746
|
// focused on index health and avoid the N-project LanceDB scans.
|
|
1595
1747
|
const lines = [
|
|
1596
|
-
`Index: ~/.gmax/lancedb (${
|
|
1748
|
+
`Index: ~/.gmax/lancedb (${chunks} chunks, ${files} files)`,
|
|
1597
1749
|
`Configured embedding: ${identity.configured.tier} ${identity.configured.vectorDim}d [${(0, embedding_status_1.embeddingFingerprintLabel)(identity.configured.fingerprint)}] (${globalConfig.embedMode})`,
|
|
1598
1750
|
identity.built
|
|
1599
1751
|
? `Built embedding: ${identity.built.tier} ${identity.built.vectorDim}d [${(0, embedding_status_1.embeddingFingerprintLabel)(identity.built.fingerprint)}] (${identity.state})`
|
|
@@ -1607,8 +1759,7 @@ exports.mcp = new commander_1.Command("mcp")
|
|
|
1607
1759
|
return ok(lines.join("\n"));
|
|
1608
1760
|
}
|
|
1609
1761
|
catch (e) {
|
|
1610
|
-
|
|
1611
|
-
return err(`Status check failed: ${msg}`);
|
|
1762
|
+
return toolError("Status check failed", e);
|
|
1612
1763
|
}
|
|
1613
1764
|
});
|
|
1614
1765
|
}
|
|
@@ -1623,8 +1774,15 @@ exports.mcp = new commander_1.Command("mcp")
|
|
|
1623
1774
|
const prefix = dir.endsWith("/") ? dir : `${dir}/`;
|
|
1624
1775
|
const limit = Math.min(Math.max(Number(args.limit) || 200, 1), 5000);
|
|
1625
1776
|
try {
|
|
1626
|
-
|
|
1627
|
-
|
|
1777
|
+
// `generateSummaries` is a deliberate no-op stub — the summarizer is
|
|
1778
|
+
// decommissioned (see the HARD STOP at the top of CLAUDE.md) — and it
|
|
1779
|
+
// touches neither the VectorDB it is handed nor any other store.
|
|
1780
|
+
// Opening one here would take a reader lease under ~/.gmax purely to
|
|
1781
|
+
// reach a function that returns {0, 0}, which is precisely the
|
|
1782
|
+
// per-session reader this tool set no longer holds. It is still called
|
|
1783
|
+
// rather than inlined so the stub stays the single source of truth.
|
|
1784
|
+
const noStoreNeeded = null;
|
|
1785
|
+
const { summarized, remaining } = yield (0, syncer_1.generateSummaries)(noStoreNeeded, prefix, (done, total) => {
|
|
1628
1786
|
console.log(`[summarize] ${done}/${total} chunks`);
|
|
1629
1787
|
}, limit);
|
|
1630
1788
|
if (summarized === 0) {
|
|
@@ -1636,8 +1794,7 @@ exports.mcp = new commander_1.Command("mcp")
|
|
|
1636
1794
|
return ok(`Summarized ${summarized} chunks in ${path.basename(dir)}/${remainMsg}`);
|
|
1637
1795
|
}
|
|
1638
1796
|
catch (e) {
|
|
1639
|
-
|
|
1640
|
-
return err(`Summarization failed: ${msg}`);
|
|
1797
|
+
return toolError("Summarization failed", e);
|
|
1641
1798
|
}
|
|
1642
1799
|
});
|
|
1643
1800
|
}
|
|
@@ -1647,117 +1804,54 @@ exports.mcp = new commander_1.Command("mcp")
|
|
|
1647
1804
|
const { proj, root } = resolveRegisteredProject(args.root);
|
|
1648
1805
|
if (!proj)
|
|
1649
1806
|
return err("Project not added to gmax yet.");
|
|
1650
|
-
const prefix = root.endsWith("/") ? root : `${root}/`;
|
|
1651
1807
|
const projectName = path.basename(root);
|
|
1652
1808
|
try {
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
.
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
"defined_symbols",
|
|
1663
|
-
"referenced_symbols",
|
|
1664
|
-
])
|
|
1665
|
-
.where((0, filter_builder_1.pathStartsWith)(prefix))
|
|
1666
|
-
.limit(200000)
|
|
1667
|
-
.toArray();
|
|
1668
|
-
if (rows.length === 0) {
|
|
1809
|
+
// `rows.project` returns the finished overview, not the up-to-200 000
|
|
1810
|
+
// rows it was computed from — the same aggregation `gmax project` runs,
|
|
1811
|
+
// in whichever process holds the store.
|
|
1812
|
+
const overview = yield readStore("project", {
|
|
1813
|
+
daemon: () => (0, daemon_client_1.sendDaemonCommand)({ cmd: "rows.project", projectRoot: root }, { timeoutMs: 120000 }),
|
|
1814
|
+
render: (resp) => resp.overview,
|
|
1815
|
+
inProcess: () => localStore((deps) => (0, rows_handler_1.runProject)(deps, root)),
|
|
1816
|
+
});
|
|
1817
|
+
if (overview.chunks === 0) {
|
|
1669
1818
|
return ok(`No indexed data found for ${root}. Run: gmax index --path ${root}`);
|
|
1670
1819
|
}
|
|
1671
|
-
const
|
|
1672
|
-
const extCounts = new Map();
|
|
1673
|
-
const dirCounts = new Map();
|
|
1674
|
-
const roleCounts = new Map();
|
|
1675
|
-
const symbolRefs = new Map();
|
|
1676
|
-
const entryPoints = [];
|
|
1677
|
-
for (const row of rows) {
|
|
1678
|
-
const p = String(row.path || "");
|
|
1679
|
-
const role = String(row.role || "IMPLEMENTATION");
|
|
1680
|
-
const exported = Boolean(row.is_exported);
|
|
1681
|
-
const complexity = Number(row.complexity || 0);
|
|
1682
|
-
const defs = toStringArray(row.defined_symbols);
|
|
1683
|
-
const refs = toStringArray(row.referenced_symbols);
|
|
1684
|
-
files.add(p);
|
|
1685
|
-
const ext = path.extname(p).toLowerCase() || path.basename(p);
|
|
1686
|
-
extCounts.set(ext, (extCounts.get(ext) || 0) + 1);
|
|
1687
|
-
const rel = p.startsWith(prefix) ? p.slice(prefix.length) : p;
|
|
1688
|
-
const parts = rel.split("/");
|
|
1689
|
-
const dir = parts.length > 2
|
|
1690
|
-
? `${parts.slice(0, 2).join("/")}/`
|
|
1691
|
-
: parts.length > 1
|
|
1692
|
-
? `${parts[0]}/`
|
|
1693
|
-
: "(root)";
|
|
1694
|
-
if (!dirCounts.has(dir)) {
|
|
1695
|
-
dirCounts.set(dir, { files: new Set(), chunks: 0 });
|
|
1696
|
-
}
|
|
1697
|
-
const dc = dirCounts.get(dir);
|
|
1698
|
-
dc.files.add(p);
|
|
1699
|
-
dc.chunks++;
|
|
1700
|
-
roleCounts.set(role, (roleCounts.get(role) || 0) + 1);
|
|
1701
|
-
for (const ref of refs) {
|
|
1702
|
-
symbolRefs.set(ref, (symbolRefs.get(ref) || 0) + 1);
|
|
1703
|
-
}
|
|
1704
|
-
if (exported &&
|
|
1705
|
-
role === "ORCHESTRATION" &&
|
|
1706
|
-
complexity >= 5 &&
|
|
1707
|
-
defs.length > 0) {
|
|
1708
|
-
const relPath = p.startsWith(prefix) ? p.slice(prefix.length) : p;
|
|
1709
|
-
entryPoints.push({ symbol: defs[0], path: relPath });
|
|
1710
|
-
}
|
|
1711
|
-
}
|
|
1820
|
+
const pct = (count) => Math.round((count / overview.chunks) * 100);
|
|
1712
1821
|
const lines = [];
|
|
1713
|
-
const projects = (0, project_registry_1.listProjects)();
|
|
1714
|
-
const proj = projects.find((p) => p.root === root);
|
|
1715
1822
|
lines.push(`Project: ${projectName} (${root})`);
|
|
1716
|
-
lines.push(`Last indexed: ${(_a = proj
|
|
1823
|
+
lines.push(`Last indexed: ${(_a = proj.lastIndexed) !== null && _a !== void 0 ? _a : "unknown"} • ${overview.chunks} chunks • ${overview.files} files`);
|
|
1717
1824
|
lines.push("");
|
|
1718
|
-
|
|
1719
|
-
.
|
|
1720
|
-
.
|
|
1721
|
-
const langLine = extEntries
|
|
1722
|
-
.map(([ext, count]) => `${ext} (${Math.round((count / rows.length) * 100)}%)`)
|
|
1723
|
-
.join(", ");
|
|
1724
|
-
lines.push(`Languages: ${langLine}`);
|
|
1825
|
+
lines.push(`Languages: ${overview.extEntries
|
|
1826
|
+
.map(([ext, count]) => `${ext} (${pct(count)}%)`)
|
|
1827
|
+
.join(", ")}`);
|
|
1725
1828
|
lines.push("");
|
|
1726
1829
|
lines.push("Directory structure:");
|
|
1727
|
-
const
|
|
1728
|
-
.
|
|
1729
|
-
.slice(0, 12);
|
|
1730
|
-
for (const [dir, data] of dirEntries) {
|
|
1731
|
-
lines.push(` ${dir.padEnd(25)} (${data.files.size} files, ${data.chunks} chunks)`);
|
|
1830
|
+
for (const [dir, data] of overview.dirEntries) {
|
|
1831
|
+
lines.push(` ${dir.padEnd(25)} (${data.files} files, ${data.chunks} chunks)`);
|
|
1732
1832
|
}
|
|
1733
1833
|
lines.push("");
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
.
|
|
1737
|
-
.join(", ");
|
|
1738
|
-
lines.push(`Roles: ${roleLine}`);
|
|
1834
|
+
lines.push(`Roles: ${overview.roleEntries
|
|
1835
|
+
.map(([role, count]) => `${pct(count)}% ${role}`)
|
|
1836
|
+
.join(", ")}`);
|
|
1739
1837
|
lines.push("");
|
|
1740
|
-
|
|
1741
|
-
.sort((a, b) => b[1] - a[1])
|
|
1742
|
-
.slice(0, 8);
|
|
1743
|
-
if (topSymbols.length > 0) {
|
|
1838
|
+
if (overview.topSymbols.length > 0) {
|
|
1744
1839
|
lines.push("Key symbols (by reference count):");
|
|
1745
|
-
for (const [sym, count] of topSymbols) {
|
|
1840
|
+
for (const [sym, count] of overview.topSymbols) {
|
|
1746
1841
|
lines.push(` ${sym.padEnd(25)} (referenced ${count}x)`);
|
|
1747
1842
|
}
|
|
1748
1843
|
lines.push("");
|
|
1749
1844
|
}
|
|
1750
|
-
if (entryPoints.length > 0) {
|
|
1845
|
+
if (overview.entryPoints.length > 0) {
|
|
1751
1846
|
lines.push("Entry points (exported orchestration):");
|
|
1752
|
-
for (const ep of entryPoints
|
|
1847
|
+
for (const ep of overview.entryPoints) {
|
|
1753
1848
|
lines.push(` ${ep.symbol.padEnd(25)} ${ep.path}`);
|
|
1754
1849
|
}
|
|
1755
1850
|
}
|
|
1756
1851
|
return ok(lines.join("\n"));
|
|
1757
1852
|
}
|
|
1758
1853
|
catch (e) {
|
|
1759
|
-
|
|
1760
|
-
return err(`Project summary failed: ${msg}`);
|
|
1854
|
+
return toolError("Project summary failed", e);
|
|
1761
1855
|
}
|
|
1762
1856
|
});
|
|
1763
1857
|
}
|
|
@@ -1776,14 +1870,13 @@ exports.mcp = new commander_1.Command("mcp")
|
|
|
1776
1870
|
});
|
|
1777
1871
|
const rootPrefix = root.endsWith("/") ? root : `${root}/`;
|
|
1778
1872
|
try {
|
|
1779
|
-
|
|
1780
|
-
const
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
if (fileChunks.length === 0) {
|
|
1873
|
+
// The file's own chunks, unscoped: the file was named explicitly.
|
|
1874
|
+
const [fileChunks] = yield locateRows(root, {
|
|
1875
|
+
select: ["defined_symbols", "referenced_symbols"],
|
|
1876
|
+
matches: [{ kind: "path", path: absPath }],
|
|
1877
|
+
scoped: false,
|
|
1878
|
+
});
|
|
1879
|
+
if (!fileChunks || fileChunks.length === 0) {
|
|
1787
1880
|
return ok(`File not found in index: ${file}. Check that the path is relative to the project root. Run \`gmax status\` to see indexed projects.`);
|
|
1788
1881
|
}
|
|
1789
1882
|
const definedHere = new Set();
|
|
@@ -1794,17 +1887,19 @@ exports.mcp = new commander_1.Command("mcp")
|
|
|
1794
1887
|
for (const s of toStringArray(chunk.referenced_symbols))
|
|
1795
1888
|
referencedHere.add(s);
|
|
1796
1889
|
}
|
|
1797
|
-
// Dependencies: files that DEFINE symbols this file REFERENCES
|
|
1890
|
+
// Dependencies: files that DEFINE symbols this file REFERENCES. One
|
|
1891
|
+
// batched round trip instead of one per symbol — the daemon runs the
|
|
1892
|
+
// same N selects, the socket carries one request.
|
|
1893
|
+
const depSymbols = [...referencedHere].filter((s) => !definedHere.has(s));
|
|
1798
1894
|
const depCounts = new Map();
|
|
1799
|
-
for (const
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
.toArray();
|
|
1895
|
+
for (const rows of yield locateRows(root, {
|
|
1896
|
+
select: ["path"],
|
|
1897
|
+
matches: depSymbols.map((symbol) => ({
|
|
1898
|
+
kind: "definedSymbol",
|
|
1899
|
+
symbol,
|
|
1900
|
+
})),
|
|
1901
|
+
limit: 3,
|
|
1902
|
+
})) {
|
|
1808
1903
|
for (const row of rows) {
|
|
1809
1904
|
const p = String(row.path || "");
|
|
1810
1905
|
if (p === absPath)
|
|
@@ -1812,15 +1907,16 @@ exports.mcp = new commander_1.Command("mcp")
|
|
|
1812
1907
|
depCounts.set(p, (depCounts.get(p) || 0) + 1);
|
|
1813
1908
|
}
|
|
1814
1909
|
}
|
|
1815
|
-
// Dependents: files that REFERENCE symbols this file DEFINES
|
|
1910
|
+
// Dependents: files that REFERENCE symbols this file DEFINES.
|
|
1816
1911
|
const revCounts = new Map();
|
|
1817
|
-
for (const
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1912
|
+
for (const rows of yield locateRows(root, {
|
|
1913
|
+
select: ["path"],
|
|
1914
|
+
matches: [...definedHere].map((symbol) => ({
|
|
1915
|
+
kind: "referencedSymbol",
|
|
1916
|
+
symbol,
|
|
1917
|
+
})),
|
|
1918
|
+
limit: 20,
|
|
1919
|
+
})) {
|
|
1824
1920
|
for (const row of rows) {
|
|
1825
1921
|
const p = String(row.path || "");
|
|
1826
1922
|
if (p === absPath)
|
|
@@ -1864,8 +1960,7 @@ exports.mcp = new commander_1.Command("mcp")
|
|
|
1864
1960
|
return ok(lines.join("\n"));
|
|
1865
1961
|
}
|
|
1866
1962
|
catch (e) {
|
|
1867
|
-
|
|
1868
|
-
return err(`Related files failed: ${msg}`);
|
|
1963
|
+
return toolError("Related files failed", e);
|
|
1869
1964
|
}
|
|
1870
1965
|
});
|
|
1871
1966
|
}
|
|
@@ -1930,7 +2025,6 @@ exports.mcp = new commander_1.Command("mcp")
|
|
|
1930
2025
|
// --- New command handlers (Phase 4) ---
|
|
1931
2026
|
function handleDiffChanges(args) {
|
|
1932
2027
|
return __awaiter(this, void 0, void 0, function* () {
|
|
1933
|
-
var _a;
|
|
1934
2028
|
ensureWatcher();
|
|
1935
2029
|
const { getChangedFiles } = yield Promise.resolve().then(() => __importStar(require("../lib/utils/git")));
|
|
1936
2030
|
const ref = typeof args.ref === "string" ? args.ref : undefined;
|
|
@@ -1953,8 +2047,8 @@ exports.mcp = new commander_1.Command("mcp")
|
|
|
1953
2047
|
const searchOptions = {
|
|
1954
2048
|
rerank: process.env.GMAX_RERANK === "1",
|
|
1955
2049
|
};
|
|
1956
|
-
const response =
|
|
1957
|
-
limit, filters: {}, pathPrefix: root }, searchOptions))
|
|
2050
|
+
const response = yield searchStore(Object.assign({ projectRoot: root, query,
|
|
2051
|
+
limit, filters: {}, pathPrefix: root }, searchOptions));
|
|
1958
2052
|
const changedSet = new Set(changedFiles);
|
|
1959
2053
|
// searcher.search() returns mapped chunks (path under metadata.path);
|
|
1960
2054
|
// changedFiles are absolute, so match on the resolved absolute path.
|
|
@@ -1972,25 +2066,32 @@ exports.mcp = new commander_1.Command("mcp")
|
|
|
1972
2066
|
});
|
|
1973
2067
|
return ok(lines.join("\n"));
|
|
1974
2068
|
}
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
2069
|
+
// One matcher per changed file, batched. `rows.locate` accepts up to
|
|
2070
|
+
// 2000 matchers per request, so a very large diff is sent in chunks
|
|
2071
|
+
// rather than one oversized line.
|
|
2072
|
+
const MATCH_BATCH = 500;
|
|
2073
|
+
const perFile = [];
|
|
2074
|
+
for (let i = 0; i < changedFiles.length; i += MATCH_BATCH) {
|
|
2075
|
+
perFile.push(...(yield locateRows(root, {
|
|
2076
|
+
select: ["defined_symbols", "role"],
|
|
2077
|
+
matches: changedFiles
|
|
2078
|
+
.slice(i, i + MATCH_BATCH)
|
|
2079
|
+
.map((file) => ({ kind: "path", path: file })),
|
|
2080
|
+
limit: 50,
|
|
2081
|
+
scoped: false,
|
|
2082
|
+
})));
|
|
2083
|
+
}
|
|
2084
|
+
const lines = changedFiles.map((file, i) => {
|
|
2085
|
+
var _a;
|
|
2086
|
+
const symbols = ((_a = perFile[i]) !== null && _a !== void 0 ? _a : []).flatMap((c) => toStringArray(c.defined_symbols));
|
|
2087
|
+
return symbols.length > 0
|
|
1987
2088
|
? `${rel(file)} (${symbols.slice(0, 5).join(", ")}${symbols.length > 5 ? "..." : ""})`
|
|
1988
|
-
: rel(file)
|
|
1989
|
-
}
|
|
2089
|
+
: rel(file);
|
|
2090
|
+
});
|
|
1990
2091
|
return ok(`${changedFiles.length} changed file${changedFiles.length === 1 ? "" : "s"}${ref ? ` (vs ${ref})` : ""}:\n${lines.join("\n")}`);
|
|
1991
2092
|
}
|
|
1992
2093
|
catch (e) {
|
|
1993
|
-
return
|
|
2094
|
+
return toolError("Diff failed", e);
|
|
1994
2095
|
}
|
|
1995
2096
|
});
|
|
1996
2097
|
}
|
|
@@ -2006,12 +2107,10 @@ exports.mcp = new commander_1.Command("mcp")
|
|
|
2006
2107
|
if (!proj)
|
|
2007
2108
|
return err("Project not added to gmax yet.");
|
|
2008
2109
|
const scopedTarget = containMcpTarget(root, target);
|
|
2009
|
-
const {
|
|
2010
|
-
const db = getVectorDb();
|
|
2011
|
-
const { symbols, symbolFamilies } = yield resolveTargetSymbols(scopedTarget, db, mcpRootPrefix(root));
|
|
2110
|
+
const { symbols, symbolFamilies } = yield resolveTarget(root, scopedTarget);
|
|
2012
2111
|
if (symbols.length === 0)
|
|
2013
2112
|
return ok(`No symbols found for: ${target}`);
|
|
2014
|
-
const tests = yield
|
|
2113
|
+
const tests = yield findTestsForSymbols(root, symbols, depth, symbolFamilies);
|
|
2015
2114
|
if (tests.length === 0)
|
|
2016
2115
|
return ok(`No tests found for ${target}.`);
|
|
2017
2116
|
const rel = (p) => p.startsWith(`${root}/`) ? p.slice(root.length + 1) : p;
|
|
@@ -2022,7 +2121,7 @@ exports.mcp = new commander_1.Command("mcp")
|
|
|
2022
2121
|
return ok(`Tests for ${target}:\n${lines.join("\n")}`);
|
|
2023
2122
|
}
|
|
2024
2123
|
catch (e) {
|
|
2025
|
-
return
|
|
2124
|
+
return toolError("Find tests failed", e);
|
|
2026
2125
|
}
|
|
2027
2126
|
});
|
|
2028
2127
|
}
|
|
@@ -2040,22 +2139,47 @@ exports.mcp = new commander_1.Command("mcp")
|
|
|
2040
2139
|
if (!proj)
|
|
2041
2140
|
return err("Project not added to gmax yet.");
|
|
2042
2141
|
const scopedTarget = containMcpTarget(root, target);
|
|
2043
|
-
const {
|
|
2142
|
+
const { isTestPath } = yield Promise.resolve().then(() => __importStar(require("../lib/graph/impact")));
|
|
2044
2143
|
const { buildImpactRollup, formatImpactRollupAgent } = yield Promise.resolve().then(() => __importStar(require("../lib/graph/impact-rollup")));
|
|
2045
|
-
const
|
|
2046
|
-
const { symbols, resolvedAsFile, symbolFamilies } = yield resolveTargetSymbols(scopedTarget, db, root);
|
|
2144
|
+
const { symbols, resolvedAsFile, symbolFamilies } = yield resolveTarget(root, scopedTarget);
|
|
2047
2145
|
if (symbols.length === 0)
|
|
2048
2146
|
return ok(`No symbols found for: ${target}`);
|
|
2049
2147
|
const targetPath = resolvedAsFile
|
|
2050
2148
|
? (0, path_containment_1.resolveContainedPath)(root, scopedTarget)
|
|
2051
2149
|
: undefined;
|
|
2052
|
-
const excludePaths = targetPath ?
|
|
2053
|
-
|
|
2150
|
+
const excludePaths = targetPath ? [targetPath] : undefined;
|
|
2151
|
+
// The flat (non-rollup) call keeps findDependents' own default of 10;
|
|
2152
|
+
// `undefined` on the wire is what preserves it.
|
|
2153
|
+
const limit = rollup
|
|
2154
|
+
? Math.min(Math.max(top * 10, 100), 500)
|
|
2155
|
+
: undefined;
|
|
2156
|
+
const scope = projectScope(root);
|
|
2054
2157
|
const [dependents, tests] = yield Promise.all([
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2158
|
+
graphVerb("graph.dependents", {
|
|
2159
|
+
projectRoot: root,
|
|
2160
|
+
scope,
|
|
2161
|
+
payload: {
|
|
2162
|
+
symbols,
|
|
2163
|
+
detailed: rollup,
|
|
2164
|
+
excludePaths,
|
|
2165
|
+
limit,
|
|
2166
|
+
families: symbolFamilies,
|
|
2167
|
+
},
|
|
2168
|
+
render: (resp) => {
|
|
2169
|
+
var _a;
|
|
2170
|
+
return (_a = resp.dependents) !== null && _a !== void 0 ? _a : [];
|
|
2171
|
+
},
|
|
2172
|
+
inProcess: () => localStore((deps) => (0, graph_handler_1.runGraphDependents)(deps.vectorDb, {
|
|
2173
|
+
symbols,
|
|
2174
|
+
queryRoot: root,
|
|
2175
|
+
detailed: rollup,
|
|
2176
|
+
excludePaths,
|
|
2177
|
+
limit,
|
|
2178
|
+
excludePrefixes: scope.excludePrefixes,
|
|
2179
|
+
families: (0, graph_handler_1.decodeSymbolFamilies)(symbolFamilies),
|
|
2180
|
+
})),
|
|
2181
|
+
}),
|
|
2182
|
+
findTestsForSymbols(root, symbols, depth, symbolFamilies),
|
|
2059
2183
|
]);
|
|
2060
2184
|
if (rollup) {
|
|
2061
2185
|
const detailedDependents = dependents;
|
|
@@ -2097,7 +2221,7 @@ exports.mcp = new commander_1.Command("mcp")
|
|
|
2097
2221
|
return ok(sections.join("\n"));
|
|
2098
2222
|
}
|
|
2099
2223
|
catch (e) {
|
|
2100
|
-
return
|
|
2224
|
+
return toolError("Impact analysis failed", e);
|
|
2101
2225
|
}
|
|
2102
2226
|
});
|
|
2103
2227
|
}
|
|
@@ -2114,67 +2238,60 @@ exports.mcp = new commander_1.Command("mcp")
|
|
|
2114
2238
|
if (!proj)
|
|
2115
2239
|
return err("Project not added to gmax yet.");
|
|
2116
2240
|
(0, embedding_status_1.assertEmbeddingSearchCompatible)([proj], (0, index_config_1.readGlobalConfig)());
|
|
2117
|
-
const db = getVectorDb();
|
|
2118
|
-
const table = yield db.ensureTable();
|
|
2119
2241
|
const isFile = target.includes("/") ||
|
|
2120
2242
|
(target.includes(".") && !target.includes(" "));
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
.
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2243
|
+
// Resolved here: containment is this process's filesystem question.
|
|
2244
|
+
const absPath = isFile ? (0, path_containment_1.resolveContainedPath)(root, target) : undefined;
|
|
2245
|
+
const scope = projectScope(root);
|
|
2246
|
+
// The source chunk's vector never leaves the store: the daemon looks it
|
|
2247
|
+
// up, runs the vector search, and returns the ranked chunks.
|
|
2248
|
+
const result = yield readStore("similar", {
|
|
2249
|
+
daemon: () => (0, daemon_client_1.sendDaemonCommand)({
|
|
2250
|
+
cmd: "vector.similar",
|
|
2251
|
+
projectRoot: root,
|
|
2252
|
+
absPath,
|
|
2253
|
+
symbol: isFile ? undefined : target,
|
|
2254
|
+
scope: (0, rows_handler_1.scopeToWire)(scope),
|
|
2255
|
+
limit,
|
|
2256
|
+
threshold,
|
|
2257
|
+
}, { timeoutMs: 60000 }),
|
|
2258
|
+
render: (resp) => resp,
|
|
2259
|
+
inProcess: () => localStore((deps) => (0, vector_handler_1.runSimilar)(deps, {
|
|
2260
|
+
projectRoot: root,
|
|
2261
|
+
absPath,
|
|
2262
|
+
symbol: isFile ? undefined : target,
|
|
2263
|
+
scope,
|
|
2264
|
+
limit,
|
|
2265
|
+
threshold,
|
|
2266
|
+
})),
|
|
2267
|
+
});
|
|
2268
|
+
if (result.status === "not-found") {
|
|
2140
2269
|
return ok(isFile
|
|
2141
2270
|
? `File not found: ${target}`
|
|
2142
2271
|
: `Symbol not found: ${target}`);
|
|
2143
|
-
|
|
2144
|
-
if (
|
|
2272
|
+
}
|
|
2273
|
+
if (result.status === "no-vector") {
|
|
2145
2274
|
return ok("Source chunk has no embedding.");
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
"path",
|
|
2149
|
-
"start_line",
|
|
2150
|
-
"defined_symbols",
|
|
2151
|
-
"role",
|
|
2152
|
-
"_distance",
|
|
2153
|
-
])
|
|
2154
|
-
.where((0, filter_builder_1.pathStartsWith)(`${root}/`))
|
|
2155
|
-
.limit(limit + 5)
|
|
2156
|
-
.toArray();
|
|
2157
|
-
let filtered = results.filter((r) => !(r.path === source.path && r.start_line === source.start_line));
|
|
2158
|
-
if (threshold > 0)
|
|
2159
|
-
filtered = filtered.filter((r) => { var _a; return 1 / (1 + ((_a = r._distance) !== null && _a !== void 0 ? _a : 0)) >= threshold; });
|
|
2160
|
-
if (filtered.length === 0)
|
|
2275
|
+
}
|
|
2276
|
+
if (result.results.length === 0) {
|
|
2161
2277
|
return ok(`No similar code found for ${target}.`);
|
|
2278
|
+
}
|
|
2162
2279
|
const rel = (p) => p.startsWith(`${root}/`) ? p.slice(root.length + 1) : p;
|
|
2163
|
-
const lines =
|
|
2164
|
-
var _a
|
|
2165
|
-
const sym = (
|
|
2166
|
-
return `${rel(r.path)}:${
|
|
2280
|
+
const lines = result.results.slice(0, limit).map((r) => {
|
|
2281
|
+
var _a;
|
|
2282
|
+
const sym = (_a = r.defined_symbols[0]) !== null && _a !== void 0 ? _a : "";
|
|
2283
|
+
return `${rel(r.path)}:${r.start_line + 1} ${sym} [${r.role || "IMPL"}] d=${r._distance.toFixed(3)}`;
|
|
2167
2284
|
});
|
|
2168
2285
|
return ok(`Similar to ${target}:\n${lines.join("\n")}`);
|
|
2169
2286
|
}
|
|
2170
2287
|
catch (e) {
|
|
2171
|
-
return
|
|
2288
|
+
return toolError("Similar search failed", e);
|
|
2172
2289
|
}
|
|
2173
2290
|
});
|
|
2174
2291
|
}
|
|
2175
2292
|
function handleBuildContext(args) {
|
|
2176
2293
|
return __awaiter(this, void 0, void 0, function* () {
|
|
2177
|
-
var _a, _b
|
|
2294
|
+
var _a, _b;
|
|
2178
2295
|
ensureWatcher();
|
|
2179
2296
|
const topic = String(args.topic || "");
|
|
2180
2297
|
if (!topic)
|
|
@@ -2187,7 +2304,7 @@ exports.mcp = new commander_1.Command("mcp")
|
|
|
2187
2304
|
return err("Project not added to gmax yet.");
|
|
2188
2305
|
(0, embedding_status_1.assertEmbeddingSearchCompatible)([proj], (0, index_config_1.readGlobalConfig)());
|
|
2189
2306
|
const searchOptions = { rerank: process.env.GMAX_RERANK === "1" };
|
|
2190
|
-
const response =
|
|
2307
|
+
const response = yield searchStore(Object.assign({ projectRoot: root, query: topic, limit, filters: {}, pathPrefix: root }, searchOptions));
|
|
2191
2308
|
if (response.data.length === 0)
|
|
2192
2309
|
return ok(`No results found for "${topic}".`);
|
|
2193
2310
|
const rel = (p) => p.startsWith(`${root}/`) ? p.slice(root.length + 1) : p;
|
|
@@ -2209,7 +2326,7 @@ exports.mcp = new commander_1.Command("mcp")
|
|
|
2209
2326
|
const absP = searchResultPath(r);
|
|
2210
2327
|
const startLine = searchResultStartLine(r);
|
|
2211
2328
|
const endLine = searchResultEndLine(r, startLine);
|
|
2212
|
-
const sym = (
|
|
2329
|
+
const sym = (_b = (_a = toStringArray(r.defined_symbols)) === null || _a === void 0 ? void 0 : _a[0]) !== null && _b !== void 0 ? _b : "";
|
|
2213
2330
|
try {
|
|
2214
2331
|
const sourcePath = (0, path_containment_1.resolveContainedFile)(root, absP);
|
|
2215
2332
|
const content = fs.readFileSync(sourcePath, "utf-8");
|
|
@@ -2223,7 +2340,7 @@ exports.mcp = new commander_1.Command("mcp")
|
|
|
2223
2340
|
sections.push(blob);
|
|
2224
2341
|
tokensUsed += estTokens(blob);
|
|
2225
2342
|
}
|
|
2226
|
-
catch (
|
|
2343
|
+
catch (_c) {
|
|
2227
2344
|
/* skip unreadable */
|
|
2228
2345
|
}
|
|
2229
2346
|
}
|
|
@@ -2231,7 +2348,7 @@ exports.mcp = new commander_1.Command("mcp")
|
|
|
2231
2348
|
return ok(sections.join("\n"));
|
|
2232
2349
|
}
|
|
2233
2350
|
catch (e) {
|
|
2234
|
-
return
|
|
2351
|
+
return toolError("Context generation failed", e);
|
|
2235
2352
|
}
|
|
2236
2353
|
});
|
|
2237
2354
|
}
|
|
@@ -2301,20 +2418,51 @@ exports.mcp = new commander_1.Command("mcp")
|
|
|
2301
2418
|
ensureWatcher();
|
|
2302
2419
|
const commitRef = String(args.commit || "HEAD");
|
|
2303
2420
|
try {
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2421
|
+
// `gatherRiskInputs` mixes git and the graph. The git half stays here —
|
|
2422
|
+
// the diff and the churn count both need a working tree — and only the
|
|
2423
|
+
// graph half (callers, definition, tests) goes to the daemon, the same
|
|
2424
|
+
// split `extract` makes when it reads a body locally after the daemon
|
|
2425
|
+
// returns the location.
|
|
2426
|
+
const { extractDiff, extractSymbols, fileChurn } = yield Promise.resolve().then(() => __importStar(require("../lib/llm/diff")));
|
|
2427
|
+
const { computeRiskTable, formatRiskTable } = yield Promise.resolve().then(() => __importStar(require("../lib/review/risk")));
|
|
2428
|
+
const diff = extractDiff(commitRef, projectRoot);
|
|
2429
|
+
const symbols = diff ? extractSymbols(diff) : [];
|
|
2430
|
+
if (symbols.length === 0) {
|
|
2431
|
+
return ok("(no changed symbols in this diff)");
|
|
2432
|
+
}
|
|
2433
|
+
const scope = projectScope(projectRoot);
|
|
2434
|
+
const facts = yield graphVerb("graph.risk", {
|
|
2435
|
+
projectRoot,
|
|
2436
|
+
scope,
|
|
2437
|
+
payload: { symbols },
|
|
2438
|
+
render: (resp) => { var _a; return (_a = resp.facts) !== null && _a !== void 0 ? _a : []; },
|
|
2439
|
+
inProcess: () => localStore((deps) => (0, graph_handler_1.runGraphRisk)(deps.vectorDb, {
|
|
2440
|
+
symbols,
|
|
2441
|
+
queryRoot: projectRoot,
|
|
2442
|
+
scope,
|
|
2443
|
+
})),
|
|
2310
2444
|
});
|
|
2311
|
-
const
|
|
2445
|
+
const prefix = projectRoot.endsWith("/")
|
|
2446
|
+
? projectRoot
|
|
2447
|
+
: `${projectRoot}/`;
|
|
2448
|
+
const rows = computeRiskTable(facts.map((fact) => ({
|
|
2449
|
+
symbol: fact.symbol,
|
|
2450
|
+
file: fact.file
|
|
2451
|
+
? fact.file.startsWith(prefix)
|
|
2452
|
+
? fact.file.slice(prefix.length)
|
|
2453
|
+
: fact.file
|
|
2454
|
+
: "(unindexed)",
|
|
2455
|
+
line: fact.line,
|
|
2456
|
+
callerCount: fact.callerCount,
|
|
2457
|
+
hasTests: fact.hasTests,
|
|
2458
|
+
churn: fileChurn(fact.file, projectRoot),
|
|
2459
|
+
})));
|
|
2312
2460
|
return rows.length === 0
|
|
2313
2461
|
? ok("(no changed symbols in this diff)")
|
|
2314
2462
|
: ok(formatRiskTable(rows, { agent: true }));
|
|
2315
2463
|
}
|
|
2316
2464
|
catch (e) {
|
|
2317
|
-
return
|
|
2465
|
+
return toolError("Risk ranking failed", e);
|
|
2318
2466
|
}
|
|
2319
2467
|
});
|
|
2320
2468
|
}
|