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.
Files changed (36) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/README.md +40 -0
  3. package/dist/commands/audit.js +28 -260
  4. package/dist/commands/context.js +42 -26
  5. package/dist/commands/dead.js +37 -35
  6. package/dist/commands/doctor.js +280 -10
  7. package/dist/commands/extract.js +79 -49
  8. package/dist/commands/impact.js +74 -20
  9. package/dist/commands/log.js +25 -25
  10. package/dist/commands/mcp.js +622 -474
  11. package/dist/commands/peek.js +49 -47
  12. package/dist/commands/project.js +29 -107
  13. package/dist/commands/related.js +43 -52
  14. package/dist/commands/search-run.js +23 -8
  15. package/dist/commands/search.js +7 -2
  16. package/dist/commands/similar.js +50 -76
  17. package/dist/commands/skeleton.js +137 -83
  18. package/dist/commands/status.js +90 -25
  19. package/dist/commands/surprises.js +73 -16
  20. package/dist/commands/symbols.js +58 -77
  21. package/dist/commands/test-find.js +51 -17
  22. package/dist/commands/trace.js +26 -11
  23. package/dist/lib/daemon/daemon.js +26 -0
  24. package/dist/lib/daemon/graph-handler.js +853 -0
  25. package/dist/lib/daemon/ipc-handler.js +19 -2
  26. package/dist/lib/daemon/read-verbs.js +105 -0
  27. package/dist/lib/daemon/rows-handler.js +665 -0
  28. package/dist/lib/daemon/vector-handler.js +199 -0
  29. package/dist/lib/store/vector-db.js +1 -1
  30. package/dist/lib/utils/daemon-client.js +11 -0
  31. package/dist/lib/utils/store-access.js +381 -0
  32. package/dist/lib/utils/tests-footer.js +19 -2
  33. package/dist/lib/utils/watcher-launcher.js +10 -0
  34. package/package.json +2 -2
  35. package/plugins/grepmax/.claude-plugin/plugin.json +1 -1
  36. package/scripts/postinstall.js +0 -69
@@ -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 languages_1 = require("../lib/core/languages");
78
- const graph_builder_1 = require("../lib/graph/graph-builder");
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
- function shouldFallbackMcpDaemonSearch(error) {
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 === "enoent" ||
129
- message === "econnrefused" ||
130
- message.includes("project not watched") ||
131
- message.includes("daemon not ready") ||
132
- message.includes("oversize") ||
133
- message.includes("unknown command"));
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
- let _vectorDb = null;
304
- let _searcher = null;
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
- const cleanup = () => __awaiter(void 0, void 0, void 0, function* () {
308
- if (_vectorDb) {
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
- // Lazy resource accessors — all use centralized store
343
- function getVectorDb() {
344
- if (!_vectorDb)
345
- _vectorDb = new vector_db_1.VectorDB(paths.lancedbDir);
346
- return _vectorDb;
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
- function getSearcher() {
349
- if (!_searcher)
350
- _searcher = new searcher_1.Searcher(getVectorDb());
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
- function daemonSearch(args) {
354
- return __awaiter(this, void 0, void 0, function* () {
355
- var _a;
356
- const { sendDaemonCommand } = yield Promise.resolve().then(() => __importStar(require("../lib/utils/daemon-client")));
357
- const response = yield sendDaemonCommand(Object.assign({ cmd: ((_a = args.filters) === null || _a === void 0 ? void 0 : _a.projectRoots) ? "search-v2" : "search" }, args), { timeoutMs: 60000 });
358
- if (!response.ok) {
359
- if (shouldFallbackMcpDaemonSearch(response.error))
360
- return null;
361
- const detail = [response.error, response.hint]
362
- .filter((value) => typeof value === "string" && value)
363
- .join(": ");
364
- throw new Error(detail || "daemon search failed");
365
- }
366
- return {
367
- data: Array.isArray(response.data)
368
- ? response.data
369
- : [],
370
- warnings: Array.isArray(response.warnings)
371
- ? response.warnings.filter((warning) => typeof warning === "string")
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, _c;
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 = (_b = (yield daemonSearch(Object.assign({ projectRoot: resolvedRoot, query,
542
- limit, filters: effectiveFilters, pathPrefix }, searchOptions)))) !== null && _b !== void 0 ? _b : (yield getSearcher().search(query, limit, searchOptions, effectiveFilters, pathPrefix));
719
+ const result = yield searchStore(Object.assign({ projectRoot: resolvedRoot, query,
720
+ limit, filters: effectiveFilters, pathPrefix }, searchOptions));
543
721
  const allowedRoots = searchAll
544
- ? ((_c = filters.projectRoots) !== null && _c !== void 0 ? _c : [])
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 (_d) {
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
- const db = getVectorDb();
696
- const builder = new graph_builder_1.GraphBuilder(db, resolvedRoot);
697
- const graph = yield builder.buildGraph(query);
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 (_e) {
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
- const msg = e instanceof Error ? e.message : String(e);
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 cached skeleton first
981
+ // Try the stored skeleton first, via the daemon's warm store.
804
982
  try {
805
- const db = getVectorDb();
806
- const cached = yield (0, retriever_1.getStoredSkeleton)(db, absPath);
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 graph = yield builder.buildGraphMultiHop(symbol, depth);
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
- const msg = e instanceof Error ? e.message : String(e);
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
- const db = getVectorDb();
957
- const table = yield db.ensureTable();
958
- const prefix = root.endsWith("/") ? root : `${root}/`;
959
- const rows = yield table
960
- .query()
961
- .select([
962
- "path",
963
- "start_line",
964
- "end_line",
965
- "role",
966
- "is_exported",
967
- "defined_symbols",
968
- ])
969
- .where(`array_contains(defined_symbols, '${(0, filter_builder_1.escapeSqlString)(symbol)}') AND ${(0, filter_builder_1.pathStartsWith)(prefix)}`)
970
- .limit(10)
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
- const msg = e instanceof Error ? e.message : String(e);
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
- const db = getVectorDb();
1051
- const { GraphBuilder } = yield Promise.resolve().then(() => __importStar(require("../lib/graph/graph-builder")));
1052
- const builder = new GraphBuilder(db, root);
1053
- const graph = yield builder.buildGraph(symbol);
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
- // Get chunk metadata for is_exported and end_line
1060
- const table = yield db.ensureTable();
1061
- const prefix = root.endsWith("/") ? root : `${root}/`;
1062
- const metaRows = yield table
1063
- .query()
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 (_a) { }
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(multiHop.callerTree);
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
- const msg = e instanceof Error ? e.message : String(e);
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 db = getVectorDb();
1184
- const table = yield db.ensureTable();
1185
- const prefix = root.endsWith("/") ? root : `${root}/`;
1186
- const defRows = yield table
1187
- .query()
1188
- .select(["path", "start_line", "is_exported"])
1189
- .where(`array_contains(defined_symbols, '${(0, filter_builder_1.escapeSqlString)(symbol)}') AND ${(0, filter_builder_1.pathStartsWith)(prefix)}`)
1190
- .limit(1)
1191
- .toArray();
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 (callers.length === 0) {
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 = callers.slice(0, 3);
1370
+ const top = facts.topCallers;
1210
1371
  const lines = [
1211
- `LIVE ${defLoc} defines ${symbol} — ${callers.length} inbound caller${callers.length === 1 ? "" : "s"} (top ${top.length}):`,
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
- const msg = e instanceof Error ? e.message : String(e);
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
- const db = getVectorDb();
1234
- const table = yield db.ensureTable();
1235
- const rows = yield table
1236
- .query()
1237
- .select([
1238
- "path",
1239
- "start_line",
1240
- "defined_symbols",
1241
- "referenced_symbols",
1242
- "type_referenced_symbols",
1243
- "is_exported",
1244
- ])
1245
- .where((0, filter_builder_1.pathStartsWith)(prefix))
1246
- .limit(500000)
1247
- .toArray();
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
- const msg = e instanceof Error ? e.message : String(e);
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
- const db = getVectorDb();
1323
- const table = yield db.ensureTable();
1324
- const result = yield (0, surprising_connections_1.analyzeSurprisingConnections)(table, root, {
1325
- sample,
1326
- neighbors,
1327
- dirDepth,
1328
- minSimilarity,
1329
- maxRows,
1330
- includeTests: Boolean(args.include_tests),
1331
- includeEval: Boolean(args.include_eval),
1332
- in: typeof args.in === "string" ? args.in : undefined,
1333
- exclude: typeof args.exclude === "string" ? args.exclude : undefined,
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
- const msg = e instanceof Error ? e.message : String(e);
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 builder = new graph_builder_1.GraphBuilder(getVectorDb(), root);
1356
- const hits = yield builder.getNeighbors(symbol, direction, maxHops);
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
- const msg = e instanceof Error ? e.message : String(e);
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 builder = new graph_builder_1.GraphBuilder(getVectorDb(), root);
1393
- const pathSyms = yield builder.findPaths(from, to, direction, maxHops);
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
- const msg = e instanceof Error ? e.message : String(e);
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 builder = new graph_builder_1.GraphBuilder(getVectorDb(), root);
1422
- const sg = yield builder.subgraphForFiles(abs);
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
- const msg = e instanceof Error ? e.message : String(e);
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
- const db = getVectorDb();
1467
- const table = yield db.ensureTable();
1468
- const rootPrefix = root.endsWith("/") ? root : `${root}/`;
1469
- let where = `array_length(defined_symbols) > 0 AND ${(0, filter_builder_1.pathStartsWith)(rootPrefix)}`;
1470
- if (pathPrefix) {
1471
- const absPrefix = (0, path_containment_1.resolveContainedPath)(root, pathPrefix);
1472
- where += ` AND ${(0, filter_builder_1.pathStartsWith)((0, filter_builder_1.normalizePath)(absPrefix))}`;
1473
- }
1474
- const query = table
1475
- .query()
1476
- .select([
1477
- "defined_symbols",
1478
- "path",
1479
- "start_line",
1480
- "role",
1481
- "is_exported",
1482
- ])
1483
- .where(where)
1484
- .limit(pattern ? 10000 : Math.max(limit * 50, 2000));
1485
- const rows = yield query.toArray();
1486
- const map = new Map();
1487
- for (const row of rows) {
1488
- const defs = toStringArray(row.defined_symbols);
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
- return `${e.symbol}${roleTag}${expTag}\t${rel}:${e.line}`;
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
- const msg = e instanceof Error ? e.message : String(e);
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 db = getVectorDb();
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 (${stats.chunks} chunks, ${fileCount} files)`,
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
- const msg = e instanceof Error ? e.message : String(e);
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
- const db = getVectorDb();
1627
- const { summarized, remaining } = yield (0, syncer_1.generateSummaries)(db, prefix, (done, total) => {
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
- const msg = e instanceof Error ? e.message : String(e);
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
- const db = getVectorDb();
1654
- const table = yield db.ensureTable();
1655
- const rows = yield table
1656
- .query()
1657
- .select([
1658
- "path",
1659
- "role",
1660
- "is_exported",
1661
- "complexity",
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 files = new Set();
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 === null || proj === void 0 ? void 0 : proj.lastIndexed) !== null && _a !== void 0 ? _a : "unknown"} • ${rows.length} chunks • ${files.size} files`);
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
- const extEntries = Array.from(extCounts.entries())
1719
- .sort((a, b) => b[1] - a[1])
1720
- .slice(0, 8);
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 dirEntries = Array.from(dirCounts.entries())
1728
- .sort((a, b) => b[1].chunks - a[1].chunks)
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
- const roleEntries = Array.from(roleCounts.entries()).sort((a, b) => b[1] - a[1]);
1735
- const roleLine = roleEntries
1736
- .map(([role, count]) => `${Math.round((count / rows.length) * 100)}% ${role}`)
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
- const topSymbols = Array.from(symbolRefs.entries())
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.slice(0, 10)) {
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
- const msg = e instanceof Error ? e.message : String(e);
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
- const db = getVectorDb();
1780
- const table = yield db.ensureTable();
1781
- const fileChunks = yield table
1782
- .query()
1783
- .select(["defined_symbols", "referenced_symbols"])
1784
- .where(`path = '${(0, filter_builder_1.escapeSqlString)(absPath)}'`)
1785
- .toArray();
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 sym of referencedHere) {
1800
- if (definedHere.has(sym))
1801
- continue;
1802
- const rows = yield table
1803
- .query()
1804
- .select(["path"])
1805
- .where(`array_contains(defined_symbols, '${(0, filter_builder_1.escapeSqlString)(sym)}') AND ${(0, filter_builder_1.pathStartsWith)(rootPrefix)}`)
1806
- .limit(3)
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 sym of definedHere) {
1818
- const rows = yield table
1819
- .query()
1820
- .select(["path"])
1821
- .where(`array_contains(referenced_symbols, '${(0, filter_builder_1.escapeSqlString)(sym)}') AND ${(0, filter_builder_1.pathStartsWith)(rootPrefix)}`)
1822
- .limit(20)
1823
- .toArray();
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
- const msg = e instanceof Error ? e.message : String(e);
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 = (_a = (yield daemonSearch(Object.assign({ projectRoot: root, query,
1957
- limit, filters: {}, pathPrefix: root }, searchOptions)))) !== null && _a !== void 0 ? _a : (yield getSearcher().search(query, limit, searchOptions, {}, root));
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
- const db = getVectorDb();
1976
- const table = yield db.ensureTable();
1977
- const lines = [];
1978
- for (const file of changedFiles) {
1979
- const chunks = yield table
1980
- .query()
1981
- .select(["defined_symbols", "role"])
1982
- .where(`path = '${(0, filter_builder_1.escapeSqlString)(file)}'`)
1983
- .limit(50)
1984
- .toArray();
1985
- const symbols = chunks.flatMap((c) => toStringArray(c.defined_symbols));
1986
- lines.push(symbols.length > 0
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 err(`Diff failed: ${e instanceof Error ? e.message : String(e)}`);
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 { resolveTargetSymbols, findTests } = yield Promise.resolve().then(() => __importStar(require("../lib/graph/impact")));
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 findTests(symbols, db, root, depth, undefined, symbolFamilies);
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 err(`Find tests failed: ${e instanceof Error ? e.message : String(e)}`);
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 { resolveTargetSymbols, findTests, findDependents, findDependentsDetailed, isTestPath, } = yield Promise.resolve().then(() => __importStar(require("../lib/graph/impact")));
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 db = getVectorDb();
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 ? new Set([targetPath]) : undefined;
2053
- const rollupLimit = Math.min(Math.max(top * 10, 100), 500);
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
- rollup
2056
- ? findDependentsDetailed(symbols, db, root, excludePaths, rollupLimit, undefined, symbolFamilies)
2057
- : findDependents(symbols, db, root, excludePaths, undefined, undefined, symbolFamilies),
2058
- findTests(symbols, db, root, depth, undefined, symbolFamilies),
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 err(`Impact analysis failed: ${e instanceof Error ? e.message : String(e)}`);
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
- let sourceRows;
2122
- if (isFile) {
2123
- const absPath = (0, path_containment_1.resolveContainedPath)(root, target);
2124
- sourceRows = yield table
2125
- .query()
2126
- .select(["vector", "path", "start_line"])
2127
- .where(`path = '${(0, filter_builder_1.escapeSqlString)(absPath)}'`)
2128
- .limit(1)
2129
- .toArray();
2130
- }
2131
- else {
2132
- sourceRows = yield table
2133
- .query()
2134
- .select(["vector", "path", "start_line"])
2135
- .where(`array_contains(defined_symbols, '${(0, filter_builder_1.escapeSqlString)(target)}') AND ${(0, filter_builder_1.pathStartsWith)(`${root}/`)}`)
2136
- .limit(1)
2137
- .toArray();
2138
- }
2139
- if (sourceRows.length === 0)
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
- const source = sourceRows[0];
2144
- if (!source.vector || source.vector.length === 0)
2272
+ }
2273
+ if (result.status === "no-vector") {
2145
2274
  return ok("Source chunk has no embedding.");
2146
- const results = yield (0, ann_config_1.configureAnnVectorQuery)(table.vectorSearch(source.vector))
2147
- .select([
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 = filtered.slice(0, limit).map((r) => {
2164
- var _a, _b, _c, _d;
2165
- const sym = (_b = (_a = toStringArray(r.defined_symbols)) === null || _a === void 0 ? void 0 : _a[0]) !== null && _b !== void 0 ? _b : "";
2166
- return `${rel(r.path)}:${Number((_c = r.start_line) !== null && _c !== void 0 ? _c : 0) + 1} ${sym} [${r.role || "IMPL"}] d=${((_d = r._distance) !== null && _d !== void 0 ? _d : 0).toFixed(3)}`;
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 err(`Similar search failed: ${e instanceof Error ? e.message : String(e)}`);
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, _c;
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 = (_a = (yield daemonSearch(Object.assign({ projectRoot: root, query: topic, limit, filters: {}, pathPrefix: root }, searchOptions)))) !== null && _a !== void 0 ? _a : (yield getSearcher().search(topic, limit, searchOptions, {}, root));
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 = (_c = (_b = toStringArray(r.defined_symbols)) === null || _b === void 0 ? void 0 : _b[0]) !== null && _c !== void 0 ? _c : "";
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 (_d) {
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 err(`Context generation failed: ${e instanceof Error ? e.message : String(e)}`);
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
- const db = getVectorDb();
2305
- const builder = new graph_builder_1.GraphBuilder(db, projectRoot);
2306
- const { gatherRiskInputs, computeRiskTable, formatRiskTable } = yield Promise.resolve().then(() => __importStar(require("../lib/review/risk")));
2307
- const inputs = yield gatherRiskInputs(commitRef, projectRoot, {
2308
- vectorDb: db,
2309
- graphBuilder: builder,
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 rows = computeRiskTable(inputs);
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 err(`Risk ranking failed: ${e instanceof Error ? e.message : String(e)}`);
2465
+ return toolError("Risk ranking failed", e);
2318
2466
  }
2319
2467
  });
2320
2468
  }