grepmax 0.17.10 → 0.17.12
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.
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
36
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
37
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
38
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
39
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
40
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
41
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
42
|
+
});
|
|
43
|
+
};
|
|
44
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
45
|
+
exports.audit = void 0;
|
|
46
|
+
exports.computeAudit = computeAudit;
|
|
47
|
+
const commander_1 = require("commander");
|
|
48
|
+
const arrow_1 = require("../lib/utils/arrow");
|
|
49
|
+
const exit_1 = require("../lib/utils/exit");
|
|
50
|
+
const project_registry_1 = require("../lib/utils/project-registry");
|
|
51
|
+
const project_root_1 = require("../lib/utils/project-root");
|
|
52
|
+
const vector_db_1 = require("../lib/store/vector-db");
|
|
53
|
+
const useColors = process.stdout.isTTY && !process.env.NO_COLOR;
|
|
54
|
+
const style = {
|
|
55
|
+
bold: (s) => (useColors ? `\x1b[1m${s}\x1b[22m` : s),
|
|
56
|
+
dim: (s) => (useColors ? `\x1b[2m${s}\x1b[39m` : s),
|
|
57
|
+
red: (s) => (useColors ? `\x1b[31m${s}\x1b[39m` : s),
|
|
58
|
+
yellow: (s) => (useColors ? `\x1b[33m${s}\x1b[39m` : s),
|
|
59
|
+
cyan: (s) => (useColors ? `\x1b[36m${s}\x1b[39m` : s),
|
|
60
|
+
};
|
|
61
|
+
// Names too generic to be useful as god-node signal (`id`, `el`, `fn`, …).
|
|
62
|
+
const MIN_GOD_NAME_LEN = 3;
|
|
63
|
+
function rel(p, prefix) {
|
|
64
|
+
return p.startsWith(prefix) ? p.slice(prefix.length) : p;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Pure aggregation over chunk rows — no DB, no I/O. Builds the symbol→def map,
|
|
68
|
+
* cross-file inbound edges, and per-file fan-in/out, then derives god nodes
|
|
69
|
+
* (most depended-upon symbols), hub files (most depended-upon files), and
|
|
70
|
+
* dead-code candidates (non-exported symbols with zero inbound references).
|
|
71
|
+
* `top` caps each list; `deadTotal` reports the full pre-cap dead count.
|
|
72
|
+
*/
|
|
73
|
+
function computeAudit(rows, prefix, top) {
|
|
74
|
+
// First definition of a symbol wins (matches GraphBuilder semantics).
|
|
75
|
+
const defs = new Map();
|
|
76
|
+
// Distinct files that reference a symbol (cross-file inbound edges).
|
|
77
|
+
const inboundFiles = new Map();
|
|
78
|
+
const inboundTotal = new Map();
|
|
79
|
+
// Per-file aggregates.
|
|
80
|
+
const fileDefs = new Map();
|
|
81
|
+
const fileOutRefs = new Map();
|
|
82
|
+
const files = new Set();
|
|
83
|
+
for (const row of rows) {
|
|
84
|
+
const file = String(row.path || "");
|
|
85
|
+
const line = Number(row.start_line || 0);
|
|
86
|
+
const exported = Boolean(row.is_exported);
|
|
87
|
+
const defSyms = (0, arrow_1.toArr)(row.defined_symbols);
|
|
88
|
+
const refSyms = (0, arrow_1.toArr)(row.referenced_symbols);
|
|
89
|
+
files.add(file);
|
|
90
|
+
for (const s of defSyms) {
|
|
91
|
+
if (!defs.has(s)) {
|
|
92
|
+
defs.set(s, { file, line, exported, complexity: 0 });
|
|
93
|
+
}
|
|
94
|
+
if (!fileDefs.has(file))
|
|
95
|
+
fileDefs.set(file, new Set());
|
|
96
|
+
fileDefs.get(file).add(s);
|
|
97
|
+
}
|
|
98
|
+
for (const s of refSyms) {
|
|
99
|
+
if (!inboundFiles.has(s))
|
|
100
|
+
inboundFiles.set(s, new Set());
|
|
101
|
+
inboundFiles.get(s).add(file);
|
|
102
|
+
inboundTotal.set(s, (inboundTotal.get(s) || 0) + 1);
|
|
103
|
+
if (!fileOutRefs.has(file))
|
|
104
|
+
fileOutRefs.set(file, new Set());
|
|
105
|
+
fileOutRefs.get(file).add(s);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
// God nodes — in-project symbols by distinct external inbound files.
|
|
109
|
+
const godNodes = [];
|
|
110
|
+
for (const [symbol, info] of defs) {
|
|
111
|
+
if (symbol.length < MIN_GOD_NAME_LEN)
|
|
112
|
+
continue;
|
|
113
|
+
const refFiles = inboundFiles.get(symbol);
|
|
114
|
+
if (!refFiles)
|
|
115
|
+
continue;
|
|
116
|
+
let external = 0;
|
|
117
|
+
for (const f of refFiles)
|
|
118
|
+
if (f !== info.file)
|
|
119
|
+
external++;
|
|
120
|
+
if (external === 0)
|
|
121
|
+
continue;
|
|
122
|
+
godNodes.push({
|
|
123
|
+
symbol,
|
|
124
|
+
file: rel(info.file, prefix),
|
|
125
|
+
line: info.line,
|
|
126
|
+
inboundFiles: external,
|
|
127
|
+
totalRefs: inboundTotal.get(symbol) || 0,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
godNodes.sort((a, b) => b.inboundFiles - a.inboundFiles || b.totalRefs - a.totalRefs);
|
|
131
|
+
// Hub files — distinct external files depending on each file (a file G
|
|
132
|
+
// depends on F if G references any symbol F defines).
|
|
133
|
+
const hubFiles = [];
|
|
134
|
+
for (const [file, syms] of fileDefs) {
|
|
135
|
+
const dependents = new Set();
|
|
136
|
+
for (const s of syms) {
|
|
137
|
+
const refFiles = inboundFiles.get(s);
|
|
138
|
+
if (!refFiles)
|
|
139
|
+
continue;
|
|
140
|
+
for (const f of refFiles)
|
|
141
|
+
if (f !== file)
|
|
142
|
+
dependents.add(f);
|
|
143
|
+
}
|
|
144
|
+
// Fan-out: distinct referenced symbols that are defined somewhere
|
|
145
|
+
// in-project (external-library calls don't count as coupling).
|
|
146
|
+
let fanOut = 0;
|
|
147
|
+
const out = fileOutRefs.get(file);
|
|
148
|
+
if (out)
|
|
149
|
+
for (const s of out)
|
|
150
|
+
if (defs.has(s))
|
|
151
|
+
fanOut++;
|
|
152
|
+
hubFiles.push({
|
|
153
|
+
file: rel(file, prefix),
|
|
154
|
+
dependents: dependents.size,
|
|
155
|
+
defines: syms.size,
|
|
156
|
+
fanOut,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
hubFiles.sort((a, b) => b.dependents - a.dependents || b.defines - a.defines);
|
|
160
|
+
// Dead candidates — non-exported in-project symbols with zero inbound
|
|
161
|
+
// references anywhere (including their own file).
|
|
162
|
+
const deadAll = [];
|
|
163
|
+
for (const [symbol, info] of defs) {
|
|
164
|
+
if (info.exported)
|
|
165
|
+
continue;
|
|
166
|
+
if ((inboundTotal.get(symbol) || 0) > 0)
|
|
167
|
+
continue;
|
|
168
|
+
deadAll.push({ symbol, file: rel(info.file, prefix), line: info.line });
|
|
169
|
+
}
|
|
170
|
+
deadAll.sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line);
|
|
171
|
+
return {
|
|
172
|
+
scannedChunks: rows.length,
|
|
173
|
+
scannedFiles: files.size,
|
|
174
|
+
godNodes: godNodes.slice(0, top),
|
|
175
|
+
hubFiles: hubFiles.filter((h) => h.dependents > 0).slice(0, top),
|
|
176
|
+
deadCandidates: deadAll.slice(0, top),
|
|
177
|
+
deadTotal: deadAll.length,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
function formatHuman(r) {
|
|
181
|
+
const out = [];
|
|
182
|
+
out.push(`${style.bold("Audit")} ${style.dim(`— ${r.scannedChunks} chunks across ${r.scannedFiles} files`)}`);
|
|
183
|
+
out.push("");
|
|
184
|
+
out.push(style.bold("God nodes") + style.dim(" (most depended-upon symbols)"));
|
|
185
|
+
if (r.godNodes.length === 0) {
|
|
186
|
+
out.push(style.dim(" none"));
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
for (const g of r.godNodes) {
|
|
190
|
+
out.push(` ${style.cyan(g.symbol.padEnd(28))} ${style.dim(`${g.inboundFiles} files`)}, ${g.totalRefs} refs ${style.dim(`${g.file}:${g.line + 1}`)}`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
out.push("");
|
|
194
|
+
out.push(style.bold("Hub files") + style.dim(" (most depended-upon files)"));
|
|
195
|
+
if (r.hubFiles.length === 0) {
|
|
196
|
+
out.push(style.dim(" none"));
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
for (const h of r.hubFiles) {
|
|
200
|
+
out.push(` ${h.file.padEnd(44)} ${style.dim(`${h.dependents} dependents, ${h.defines} defs, fan-out ${h.fanOut}`)}`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
out.push("");
|
|
204
|
+
out.push(style.bold("Dead-code candidates") +
|
|
205
|
+
style.dim(` (${r.deadTotal} non-exported symbols with zero inbound refs)`));
|
|
206
|
+
if (r.deadCandidates.length === 0) {
|
|
207
|
+
out.push(style.dim(" none"));
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
for (const d of r.deadCandidates) {
|
|
211
|
+
out.push(` ${style.red(d.symbol.padEnd(28))} ${style.dim(`${d.file}:${d.line + 1}`)}`);
|
|
212
|
+
}
|
|
213
|
+
if (r.deadTotal > r.deadCandidates.length) {
|
|
214
|
+
out.push(style.dim(` … and ${r.deadTotal - r.deadCandidates.length} more`));
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
out.push("");
|
|
218
|
+
out.push(style.dim("Static call graph: dynamic dispatch, reflection, eval, and string-built " +
|
|
219
|
+
"call sites are invisible. Dead candidates are hypotheses — verify with " +
|
|
220
|
+
"`gmax dead <symbol>` and `grep` before removing."));
|
|
221
|
+
return out.join("\n");
|
|
222
|
+
}
|
|
223
|
+
function formatAgent(r) {
|
|
224
|
+
const lines = [];
|
|
225
|
+
lines.push(`scanned\t${r.scannedChunks}\t${r.scannedFiles}`);
|
|
226
|
+
for (const g of r.godNodes) {
|
|
227
|
+
lines.push(`god\t${g.symbol}\t${g.file}:${g.line + 1}\t${g.inboundFiles}\t${g.totalRefs}`);
|
|
228
|
+
}
|
|
229
|
+
for (const h of r.hubFiles) {
|
|
230
|
+
lines.push(`hub\t${h.file}\t${h.dependents}\t${h.defines}\t${h.fanOut}`);
|
|
231
|
+
}
|
|
232
|
+
for (const d of r.deadCandidates) {
|
|
233
|
+
lines.push(`dead\t${d.symbol}\t${d.file}:${d.line + 1}`);
|
|
234
|
+
}
|
|
235
|
+
lines.push(`dead_total\t${r.deadTotal}`);
|
|
236
|
+
return lines.join("\n");
|
|
237
|
+
}
|
|
238
|
+
exports.audit = new commander_1.Command("audit")
|
|
239
|
+
.description("Graph-summary of the indexed project — god nodes (most depended-upon " +
|
|
240
|
+
"symbols), hub files (most depended-upon files), and dead-code candidates " +
|
|
241
|
+
"(non-exported symbols with zero inbound references). One pass over the " +
|
|
242
|
+
"static call graph; dynamic dispatch / reflection / eval are invisible, so " +
|
|
243
|
+
"dead candidates are hypotheses, not proof.")
|
|
244
|
+
.option("--root <dir>", "Project root directory")
|
|
245
|
+
.option("--in <subpath>", "Restrict to a sub-path of the project (repeatable)", (value, prev) => prev ? [...prev, value] : [value])
|
|
246
|
+
.option("--exclude <subpath>", "Exclude a sub-path of the project (repeatable)", (value, prev) => prev ? [...prev, value] : [value])
|
|
247
|
+
.option("--top <n>", "How many of each category to show", "10")
|
|
248
|
+
.option("--agent", "Compact TSV output for AI agents", false)
|
|
249
|
+
.action((opts) => __awaiter(void 0, void 0, void 0, function* () {
|
|
250
|
+
var _a;
|
|
251
|
+
const root = (0, project_registry_1.resolveRootOrExit)(opts.root);
|
|
252
|
+
if (root === null)
|
|
253
|
+
return;
|
|
254
|
+
let vectorDb = null;
|
|
255
|
+
try {
|
|
256
|
+
const projectRoot = (_a = (0, project_root_1.findProjectRoot)(root)) !== null && _a !== void 0 ? _a : root;
|
|
257
|
+
const prefix = projectRoot.endsWith("/")
|
|
258
|
+
? projectRoot
|
|
259
|
+
: `${projectRoot}/`;
|
|
260
|
+
const paths = (0, project_root_1.ensureProjectPaths)(projectRoot);
|
|
261
|
+
vectorDb = new vector_db_1.VectorDB(paths.lancedbDir);
|
|
262
|
+
const { resolveScope, buildScopeWhere } = yield Promise.resolve().then(() => __importStar(require("../lib/utils/scope-filter")));
|
|
263
|
+
const scope = resolveScope({
|
|
264
|
+
projectRoot,
|
|
265
|
+
in: opts.in,
|
|
266
|
+
exclude: opts.exclude,
|
|
267
|
+
});
|
|
268
|
+
const top = Math.max(1, Number.parseInt(opts.top, 10) || 10);
|
|
269
|
+
const table = yield vectorDb.ensureTable();
|
|
270
|
+
const rows = yield table
|
|
271
|
+
.query()
|
|
272
|
+
.select([
|
|
273
|
+
"path",
|
|
274
|
+
"start_line",
|
|
275
|
+
"defined_symbols",
|
|
276
|
+
"referenced_symbols",
|
|
277
|
+
"is_exported",
|
|
278
|
+
])
|
|
279
|
+
.where(buildScopeWhere(scope))
|
|
280
|
+
.limit(500000)
|
|
281
|
+
.toArray();
|
|
282
|
+
if (rows.length === 0) {
|
|
283
|
+
console.log(opts.agent
|
|
284
|
+
? "(no indexed data)"
|
|
285
|
+
: `No indexed data for ${projectRoot}. Run: gmax index --path ${projectRoot}`);
|
|
286
|
+
process.exitCode = 1;
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
const result = computeAudit(rows.map((r) => ({
|
|
290
|
+
path: String(r.path || ""),
|
|
291
|
+
start_line: Number(r.start_line || 0),
|
|
292
|
+
is_exported: Boolean(r.is_exported),
|
|
293
|
+
defined_symbols: (0, arrow_1.toArr)(r.defined_symbols),
|
|
294
|
+
referenced_symbols: (0, arrow_1.toArr)(r.referenced_symbols),
|
|
295
|
+
})), prefix, top);
|
|
296
|
+
console.log(opts.agent ? formatAgent(result) : formatHuman(result));
|
|
297
|
+
}
|
|
298
|
+
catch (error) {
|
|
299
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
300
|
+
console.error("Audit failed:", message);
|
|
301
|
+
process.exitCode = 1;
|
|
302
|
+
}
|
|
303
|
+
finally {
|
|
304
|
+
if (vectorDb) {
|
|
305
|
+
try {
|
|
306
|
+
yield vectorDb.close();
|
|
307
|
+
}
|
|
308
|
+
catch (err) {
|
|
309
|
+
console.error("Failed to close VectorDB:", err);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
yield (0, exit_1.gracefulExit)();
|
|
313
|
+
}
|
|
314
|
+
}));
|
package/dist/commands/mcp.js
CHANGED
|
@@ -62,6 +62,7 @@ const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
|
62
62
|
const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
|
|
63
63
|
const commander_1 = require("commander");
|
|
64
64
|
const config_1 = require("../config");
|
|
65
|
+
const audit_1 = require("./audit");
|
|
65
66
|
const graph_builder_1 = require("../lib/graph/graph-builder");
|
|
66
67
|
const index_config_1 = require("../lib/index/index-config");
|
|
67
68
|
const syncer_1 = require("../lib/index/syncer");
|
|
@@ -239,6 +240,82 @@ const TOOLS = [
|
|
|
239
240
|
required: ["symbol"],
|
|
240
241
|
},
|
|
241
242
|
},
|
|
243
|
+
{
|
|
244
|
+
name: "audit",
|
|
245
|
+
description: "Graph-summary of the indexed project in one call: god nodes (most depended-upon symbols), hub files (most depended-upon files), and dead-code candidates (non-exported symbols with zero inbound references). Built from the static call graph — dynamic dispatch, reflection, eval, and type-position-only references are invisible, so dead candidates are hypotheses; verify with the `dead` tool before acting.",
|
|
246
|
+
inputSchema: {
|
|
247
|
+
type: "object",
|
|
248
|
+
properties: {
|
|
249
|
+
root: {
|
|
250
|
+
type: "string",
|
|
251
|
+
description: "Project root (default: current)",
|
|
252
|
+
},
|
|
253
|
+
top: {
|
|
254
|
+
type: "number",
|
|
255
|
+
description: "How many of each category to return (default 10)",
|
|
256
|
+
},
|
|
257
|
+
},
|
|
258
|
+
},
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
name: "get_neighbors",
|
|
262
|
+
description: "Graph primitive: symbols reachable from a node along call edges within N hops. direction 'callees' = what it calls (outbound), 'callers' = what calls it (inbound). Each result carries its hop distance and definition location. Static call graph — same caveats as `dead`/`audit`.",
|
|
263
|
+
inputSchema: {
|
|
264
|
+
type: "object",
|
|
265
|
+
properties: {
|
|
266
|
+
symbol: { type: "string", description: "Starting symbol" },
|
|
267
|
+
direction: {
|
|
268
|
+
type: "string",
|
|
269
|
+
enum: ["callers", "callees"],
|
|
270
|
+
description: "Edge direction (default callees)",
|
|
271
|
+
},
|
|
272
|
+
max_hops: {
|
|
273
|
+
type: "number",
|
|
274
|
+
description: "Max hops (default 2, max 5)",
|
|
275
|
+
},
|
|
276
|
+
root: { type: "string", description: "Project root (absolute path)" },
|
|
277
|
+
},
|
|
278
|
+
required: ["symbol"],
|
|
279
|
+
},
|
|
280
|
+
},
|
|
281
|
+
{
|
|
282
|
+
name: "find_paths",
|
|
283
|
+
description: "Graph primitive: shortest call-graph path between two symbols, as a symbol sequence. direction 'callees' searches outward from `from` (does `from` transitively call `to`?), 'callers' searches inward. Returns 'no path' if unreachable within max_hops.",
|
|
284
|
+
inputSchema: {
|
|
285
|
+
type: "object",
|
|
286
|
+
properties: {
|
|
287
|
+
from: { type: "string", description: "Start symbol" },
|
|
288
|
+
to: { type: "string", description: "Target symbol" },
|
|
289
|
+
direction: {
|
|
290
|
+
type: "string",
|
|
291
|
+
enum: ["callers", "callees"],
|
|
292
|
+
description: "Search direction (default callees)",
|
|
293
|
+
},
|
|
294
|
+
max_hops: {
|
|
295
|
+
type: "number",
|
|
296
|
+
description: "Max hops (default 6, max 10)",
|
|
297
|
+
},
|
|
298
|
+
root: { type: "string", description: "Project root (absolute path)" },
|
|
299
|
+
},
|
|
300
|
+
required: ["from", "to"],
|
|
301
|
+
},
|
|
302
|
+
},
|
|
303
|
+
{
|
|
304
|
+
name: "subgraph_for_files",
|
|
305
|
+
description: "Graph primitive: the local dependency subgraph for a set of files — symbols they define, the call edges among those symbols, and their outbound external dependencies. Paths may be absolute or project-root-relative.",
|
|
306
|
+
inputSchema: {
|
|
307
|
+
type: "object",
|
|
308
|
+
properties: {
|
|
309
|
+
files: {
|
|
310
|
+
type: "array",
|
|
311
|
+
items: { type: "string" },
|
|
312
|
+
description: "File paths (absolute or root-relative)",
|
|
313
|
+
},
|
|
314
|
+
root: { type: "string", description: "Project root (absolute path)" },
|
|
315
|
+
},
|
|
316
|
+
required: ["files"],
|
|
317
|
+
},
|
|
318
|
+
},
|
|
242
319
|
{
|
|
243
320
|
name: "list_symbols",
|
|
244
321
|
description: "List indexed symbols with role and export status.",
|
|
@@ -1338,6 +1415,181 @@ exports.mcp = new commander_1.Command("mcp")
|
|
|
1338
1415
|
}
|
|
1339
1416
|
});
|
|
1340
1417
|
}
|
|
1418
|
+
function handleAudit(args) {
|
|
1419
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
1420
|
+
ensureWatcher();
|
|
1421
|
+
const root = typeof args.root === "string" && args.root
|
|
1422
|
+
? path.resolve(args.root)
|
|
1423
|
+
: projectRoot;
|
|
1424
|
+
const prefix = root.endsWith("/") ? root : `${root}/`;
|
|
1425
|
+
const top = Math.min(Math.max(Number(args.top) || 10, 1), 50);
|
|
1426
|
+
try {
|
|
1427
|
+
const db = getVectorDb();
|
|
1428
|
+
const table = yield db.ensureTable();
|
|
1429
|
+
const rows = yield table
|
|
1430
|
+
.query()
|
|
1431
|
+
.select([
|
|
1432
|
+
"path",
|
|
1433
|
+
"start_line",
|
|
1434
|
+
"defined_symbols",
|
|
1435
|
+
"referenced_symbols",
|
|
1436
|
+
"is_exported",
|
|
1437
|
+
])
|
|
1438
|
+
.where(`path LIKE '${(0, filter_builder_1.escapeSqlString)(prefix)}%'`)
|
|
1439
|
+
.limit(500000)
|
|
1440
|
+
.toArray();
|
|
1441
|
+
if (rows.length === 0) {
|
|
1442
|
+
return ok(`No indexed data found for ${root}. Run: gmax index --path ${root}`);
|
|
1443
|
+
}
|
|
1444
|
+
const audit = (0, audit_1.computeAudit)(rows.map((r) => ({
|
|
1445
|
+
path: String(r.path || ""),
|
|
1446
|
+
start_line: Number(r.start_line || 0),
|
|
1447
|
+
is_exported: Boolean(r.is_exported),
|
|
1448
|
+
defined_symbols: toStringArray(r.defined_symbols),
|
|
1449
|
+
referenced_symbols: toStringArray(r.referenced_symbols),
|
|
1450
|
+
})), prefix, top);
|
|
1451
|
+
const lines = [];
|
|
1452
|
+
lines.push(`Audit — ${audit.scannedChunks} chunks across ${audit.scannedFiles} files`);
|
|
1453
|
+
lines.push("");
|
|
1454
|
+
lines.push("God nodes (most depended-upon symbols):");
|
|
1455
|
+
for (const g of audit.godNodes) {
|
|
1456
|
+
lines.push(` ${g.symbol} — ${g.inboundFiles} files, ${g.totalRefs} refs (${g.file}:${g.line + 1})`);
|
|
1457
|
+
}
|
|
1458
|
+
if (audit.godNodes.length === 0)
|
|
1459
|
+
lines.push(" none");
|
|
1460
|
+
lines.push("");
|
|
1461
|
+
lines.push("Hub files (most depended-upon files):");
|
|
1462
|
+
for (const h of audit.hubFiles) {
|
|
1463
|
+
lines.push(` ${h.file} — ${h.dependents} dependents, ${h.defines} defs, fan-out ${h.fanOut}`);
|
|
1464
|
+
}
|
|
1465
|
+
if (audit.hubFiles.length === 0)
|
|
1466
|
+
lines.push(" none");
|
|
1467
|
+
lines.push("");
|
|
1468
|
+
lines.push(`Dead-code candidates (${audit.deadTotal} non-exported symbols with zero inbound refs):`);
|
|
1469
|
+
for (const d of audit.deadCandidates) {
|
|
1470
|
+
lines.push(` ${d.symbol} (${d.file}:${d.line + 1})`);
|
|
1471
|
+
}
|
|
1472
|
+
if (audit.deadCandidates.length === 0)
|
|
1473
|
+
lines.push(" none");
|
|
1474
|
+
if (audit.deadTotal > audit.deadCandidates.length) {
|
|
1475
|
+
lines.push(` … and ${audit.deadTotal - audit.deadCandidates.length} more`);
|
|
1476
|
+
}
|
|
1477
|
+
lines.push("");
|
|
1478
|
+
lines.push("Static call graph: dynamic dispatch, reflection, eval, and type-position-only references are invisible. Dead candidates are hypotheses — verify with the `dead` tool before removing.");
|
|
1479
|
+
return ok(lines.join("\n"));
|
|
1480
|
+
}
|
|
1481
|
+
catch (e) {
|
|
1482
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
1483
|
+
return err(`Audit failed: ${msg}`);
|
|
1484
|
+
}
|
|
1485
|
+
});
|
|
1486
|
+
}
|
|
1487
|
+
function handleGetNeighbors(args) {
|
|
1488
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
1489
|
+
ensureWatcher();
|
|
1490
|
+
const symbol = String(args.symbol || "");
|
|
1491
|
+
if (!symbol)
|
|
1492
|
+
return err("Missing required parameter: symbol");
|
|
1493
|
+
const direction = args.direction === "callers" ? "callers" : "callees";
|
|
1494
|
+
const maxHops = Math.min(Math.max(Number(args.max_hops) || 2, 1), 5);
|
|
1495
|
+
try {
|
|
1496
|
+
const root = typeof args.root === "string" && args.root ? args.root : projectRoot;
|
|
1497
|
+
const builder = new graph_builder_1.GraphBuilder(getVectorDb(), root);
|
|
1498
|
+
const hits = yield builder.getNeighbors(symbol, direction, maxHops);
|
|
1499
|
+
if (hits.length === 0) {
|
|
1500
|
+
return ok(`No ${direction} found for '${symbol}' within ${maxHops} hop(s). Check it is indexed (\`gmax status\`) or try the \`dead\` tool.`);
|
|
1501
|
+
}
|
|
1502
|
+
const rel = (p) => p.startsWith(root) ? p.slice(root.length + 1) : p;
|
|
1503
|
+
const lines = [
|
|
1504
|
+
`${direction} of ${symbol} (≤${maxHops} hops, ${hits.length} found):`,
|
|
1505
|
+
];
|
|
1506
|
+
for (const h of hits.slice(0, 100)) {
|
|
1507
|
+
const loc = h.file ? ` ${rel(h.file)}:${h.line + 1}` : " (external)";
|
|
1508
|
+
lines.push(` [${h.hops}h] ${h.symbol}${loc}`);
|
|
1509
|
+
}
|
|
1510
|
+
if (hits.length > 100)
|
|
1511
|
+
lines.push(` … and ${hits.length - 100} more`);
|
|
1512
|
+
return ok(lines.join("\n"));
|
|
1513
|
+
}
|
|
1514
|
+
catch (e) {
|
|
1515
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
1516
|
+
return err(`get_neighbors failed: ${msg}`);
|
|
1517
|
+
}
|
|
1518
|
+
});
|
|
1519
|
+
}
|
|
1520
|
+
function handleFindPaths(args) {
|
|
1521
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
1522
|
+
ensureWatcher();
|
|
1523
|
+
const from = String(args.from || "");
|
|
1524
|
+
const to = String(args.to || "");
|
|
1525
|
+
if (!from || !to) {
|
|
1526
|
+
return err("Missing required parameters: from and to");
|
|
1527
|
+
}
|
|
1528
|
+
const direction = args.direction === "callers" ? "callers" : "callees";
|
|
1529
|
+
const maxHops = Math.min(Math.max(Number(args.max_hops) || 6, 1), 10);
|
|
1530
|
+
try {
|
|
1531
|
+
const root = typeof args.root === "string" && args.root ? args.root : projectRoot;
|
|
1532
|
+
const builder = new graph_builder_1.GraphBuilder(getVectorDb(), root);
|
|
1533
|
+
const pathSyms = yield builder.findPaths(from, to, direction, maxHops);
|
|
1534
|
+
if (!pathSyms) {
|
|
1535
|
+
return ok(`No ${direction} path from '${from}' to '${to}' within ${maxHops} hops.`);
|
|
1536
|
+
}
|
|
1537
|
+
const arrow = direction === "callees" ? " → " : " ← ";
|
|
1538
|
+
const hops = pathSyms.length - 1;
|
|
1539
|
+
return ok(`Path (${hops} hop${hops === 1 ? "" : "s"}): ${pathSyms.join(arrow)}`);
|
|
1540
|
+
}
|
|
1541
|
+
catch (e) {
|
|
1542
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
1543
|
+
return err(`find_paths failed: ${msg}`);
|
|
1544
|
+
}
|
|
1545
|
+
});
|
|
1546
|
+
}
|
|
1547
|
+
function handleSubgraphForFiles(args) {
|
|
1548
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
1549
|
+
ensureWatcher();
|
|
1550
|
+
const filesIn = Array.isArray(args.files)
|
|
1551
|
+
? args.files.map((f) => String(f)).filter(Boolean)
|
|
1552
|
+
: [];
|
|
1553
|
+
if (filesIn.length === 0) {
|
|
1554
|
+
return err("Missing required parameter: files (non-empty array)");
|
|
1555
|
+
}
|
|
1556
|
+
try {
|
|
1557
|
+
const root = typeof args.root === "string" && args.root ? args.root : projectRoot;
|
|
1558
|
+
const abs = filesIn.map((f) => path.isAbsolute(f) ? f : path.resolve(root, f));
|
|
1559
|
+
const builder = new graph_builder_1.GraphBuilder(getVectorDb(), root);
|
|
1560
|
+
const sg = yield builder.subgraphForFiles(abs);
|
|
1561
|
+
if (sg.symbols.length === 0) {
|
|
1562
|
+
return ok(`No indexed symbols found in: ${filesIn.join(", ")}. Check the paths and \`gmax status\`.`);
|
|
1563
|
+
}
|
|
1564
|
+
const rel = (p) => p.startsWith(root) ? p.slice(root.length + 1) : p;
|
|
1565
|
+
const cap = (arr, n) => arr.length > n
|
|
1566
|
+
? `${arr.slice(0, n).join(", ")} … (+${arr.length - n})`
|
|
1567
|
+
: arr.join(", ");
|
|
1568
|
+
const lines = [];
|
|
1569
|
+
lines.push(`Subgraph for ${sg.files.length} file(s): ${sg.symbols.length} symbols, ${sg.internalEdges.length} internal edges, ${sg.externalDeps.length} external deps`);
|
|
1570
|
+
lines.push(`Files: ${sg.files.map(rel).join(", ")}`);
|
|
1571
|
+
lines.push("");
|
|
1572
|
+
lines.push(`Symbols: ${cap(sg.symbols, 50)}`);
|
|
1573
|
+
lines.push("");
|
|
1574
|
+
lines.push("Internal edges:");
|
|
1575
|
+
for (const e of sg.internalEdges.slice(0, 80)) {
|
|
1576
|
+
lines.push(` ${e.from} → ${e.to}`);
|
|
1577
|
+
}
|
|
1578
|
+
if (sg.internalEdges.length === 0)
|
|
1579
|
+
lines.push(" none");
|
|
1580
|
+
if (sg.internalEdges.length > 80) {
|
|
1581
|
+
lines.push(` … and ${sg.internalEdges.length - 80} more`);
|
|
1582
|
+
}
|
|
1583
|
+
lines.push("");
|
|
1584
|
+
lines.push(`External deps: ${cap(sg.externalDeps, 50) || "none"}`);
|
|
1585
|
+
return ok(lines.join("\n"));
|
|
1586
|
+
}
|
|
1587
|
+
catch (e) {
|
|
1588
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
1589
|
+
return err(`subgraph_for_files failed: ${msg}`);
|
|
1590
|
+
}
|
|
1591
|
+
});
|
|
1592
|
+
}
|
|
1341
1593
|
function handleListSymbols(args) {
|
|
1342
1594
|
return __awaiter(this, void 0, void 0, function* () {
|
|
1343
1595
|
ensureWatcher();
|
|
@@ -2080,6 +2332,18 @@ exports.mcp = new commander_1.Command("mcp")
|
|
|
2080
2332
|
case "dead":
|
|
2081
2333
|
result = yield handleDead(toolArgs);
|
|
2082
2334
|
break;
|
|
2335
|
+
case "audit":
|
|
2336
|
+
result = yield handleAudit(toolArgs);
|
|
2337
|
+
break;
|
|
2338
|
+
case "get_neighbors":
|
|
2339
|
+
result = yield handleGetNeighbors(toolArgs);
|
|
2340
|
+
break;
|
|
2341
|
+
case "find_paths":
|
|
2342
|
+
result = yield handleFindPaths(toolArgs);
|
|
2343
|
+
break;
|
|
2344
|
+
case "subgraph_for_files":
|
|
2345
|
+
result = yield handleSubgraphForFiles(toolArgs);
|
|
2346
|
+
break;
|
|
2083
2347
|
case "list_symbols":
|
|
2084
2348
|
result = yield handleListSymbols(toolArgs);
|
|
2085
2349
|
break;
|
package/dist/index.js
CHANGED
|
@@ -39,6 +39,7 @@ const fs = __importStar(require("node:fs"));
|
|
|
39
39
|
const path = __importStar(require("node:path"));
|
|
40
40
|
const commander_1 = require("commander");
|
|
41
41
|
const add_1 = require("./commands/add");
|
|
42
|
+
const audit_1 = require("./commands/audit");
|
|
42
43
|
const context_1 = require("./commands/context");
|
|
43
44
|
const dead_1 = require("./commands/dead");
|
|
44
45
|
const diff_1 = require("./commands/diff");
|
|
@@ -116,6 +117,7 @@ commander_1.program.addCommand(trace_1.trace);
|
|
|
116
117
|
commander_1.program.addCommand(extract_1.extract);
|
|
117
118
|
commander_1.program.addCommand(peek_1.peek);
|
|
118
119
|
commander_1.program.addCommand(dead_1.dead);
|
|
120
|
+
commander_1.program.addCommand(audit_1.audit);
|
|
119
121
|
commander_1.program.addCommand(project_1.project);
|
|
120
122
|
commander_1.program.addCommand(related_1.related);
|
|
121
123
|
commander_1.program.addCommand(log_1.log);
|
|
@@ -11,6 +11,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
12
|
exports.GraphBuilder = void 0;
|
|
13
13
|
const filter_builder_1 = require("../utils/filter-builder");
|
|
14
|
+
const graph_traversal_1 = require("./graph-traversal");
|
|
14
15
|
class GraphBuilder {
|
|
15
16
|
constructor(db, pathPrefix, excludePrefixes) {
|
|
16
17
|
this.db = db;
|
|
@@ -185,6 +186,102 @@ class GraphBuilder {
|
|
|
185
186
|
return trees;
|
|
186
187
|
});
|
|
187
188
|
}
|
|
189
|
+
// --- MCP graph primitives (Phase 7) -----------------------------------
|
|
190
|
+
/** Symbols the given symbol references (outbound edges). */
|
|
191
|
+
calleesOf(symbol) {
|
|
192
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
193
|
+
return this.getCallees(symbol);
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
/** Distinct symbols that reference the given symbol (inbound edges). */
|
|
197
|
+
callersOf(symbol) {
|
|
198
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
199
|
+
const nodes = yield this.getCallers(symbol);
|
|
200
|
+
const out = [];
|
|
201
|
+
for (const n of nodes) {
|
|
202
|
+
if (n.symbol && n.symbol !== "unknown" && !out.includes(n.symbol)) {
|
|
203
|
+
out.push(n.symbol);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return out;
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
neighborFn(direction) {
|
|
210
|
+
return direction === "callers"
|
|
211
|
+
? (s) => this.callersOf(s)
|
|
212
|
+
: (s) => this.calleesOf(s);
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Symbols reachable from `symbol` along `direction` within `maxHops`, each
|
|
216
|
+
* annotated with hop distance and resolved to a definition location when one
|
|
217
|
+
* is indexed.
|
|
218
|
+
*/
|
|
219
|
+
getNeighbors(symbol, direction, maxHops) {
|
|
220
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
221
|
+
var _a, _b;
|
|
222
|
+
const hits = yield (0, graph_traversal_1.bfsNeighbors)(symbol, this.neighborFn(direction), maxHops);
|
|
223
|
+
const out = [];
|
|
224
|
+
for (const h of hits) {
|
|
225
|
+
const loc = yield this.resolveLocation(h.symbol);
|
|
226
|
+
out.push(Object.assign(Object.assign({}, h), { file: (_a = loc === null || loc === void 0 ? void 0 : loc.file) !== null && _a !== void 0 ? _a : "", line: (_b = loc === null || loc === void 0 ? void 0 : loc.line) !== null && _b !== void 0 ? _b : 0 }));
|
|
227
|
+
}
|
|
228
|
+
return out;
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
/** Shortest path `[from, …, to]` along `direction`, or null. */
|
|
232
|
+
findPaths(from_1, to_1, direction_1) {
|
|
233
|
+
return __awaiter(this, arguments, void 0, function* (from, to, direction, maxHops = 6) {
|
|
234
|
+
return (0, graph_traversal_1.findPath)(from, to, this.neighborFn(direction), maxHops);
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
/** Resolve a symbol to its first defining chunk's file:line, if indexed. */
|
|
238
|
+
resolveLocation(symbol) {
|
|
239
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
240
|
+
const table = yield this.db.ensureTable();
|
|
241
|
+
const escaped = (0, filter_builder_1.escapeSqlString)(symbol);
|
|
242
|
+
const rows = yield table
|
|
243
|
+
.query()
|
|
244
|
+
.select(["path", "start_line"])
|
|
245
|
+
.where(this.scopeWhere(`array_contains(defined_symbols, '${escaped}')`))
|
|
246
|
+
.limit(1)
|
|
247
|
+
.toArray();
|
|
248
|
+
if (rows.length === 0)
|
|
249
|
+
return null;
|
|
250
|
+
const r = rows[0];
|
|
251
|
+
return { file: String(r.path || ""), line: Number(r.start_line || 0) };
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Build the local dependency subgraph for a set of files: every symbol they
|
|
256
|
+
* define, the edges among those symbols, and their outbound external deps.
|
|
257
|
+
*/
|
|
258
|
+
subgraphForFiles(files) {
|
|
259
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
260
|
+
if (files.length === 0) {
|
|
261
|
+
return { files: [], symbols: [], internalEdges: [], externalDeps: [] };
|
|
262
|
+
}
|
|
263
|
+
const table = yield this.db.ensureTable();
|
|
264
|
+
const orClause = files
|
|
265
|
+
.map((f) => `path = '${(0, filter_builder_1.escapeSqlString)(f)}'`)
|
|
266
|
+
.join(" OR ");
|
|
267
|
+
const rows = yield table
|
|
268
|
+
.query()
|
|
269
|
+
.select(["path", "defined_symbols", "referenced_symbols"])
|
|
270
|
+
.where(this.scopeWhere(`(${orClause})`))
|
|
271
|
+
.limit(100000)
|
|
272
|
+
.toArray();
|
|
273
|
+
const toArray = (val) => {
|
|
274
|
+
if (val && typeof val.toArray === "function")
|
|
275
|
+
return val.toArray();
|
|
276
|
+
return Array.isArray(val) ? val : [];
|
|
277
|
+
};
|
|
278
|
+
return (0, graph_traversal_1.buildFileSubgraph)(rows.map((r) => ({
|
|
279
|
+
path: String(r.path || ""),
|
|
280
|
+
defined_symbols: toArray(r.defined_symbols),
|
|
281
|
+
referenced_symbols: toArray(r.referenced_symbols),
|
|
282
|
+
})));
|
|
283
|
+
});
|
|
284
|
+
}
|
|
188
285
|
mapRowToNode(row, targetSymbol, type) {
|
|
189
286
|
// Helper to convert Arrow Vector to array if needed
|
|
190
287
|
const toArray = (val) => {
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "grepmax",
|
|
3
|
-
"version": "0.17.
|
|
3
|
+
"version": "0.17.12",
|
|
4
4
|
"author": "Robert Owens <78518764+reowens@users.noreply.github.com>",
|
|
5
5
|
"homepage": "https://github.com/reowens/grepmax",
|
|
6
6
|
"bugs": {
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"prepublishOnly": "pnpm build",
|
|
38
38
|
"preversion": "pnpm test && pnpm typecheck",
|
|
39
39
|
"version": "bash scripts/sync-versions.sh && git add -A",
|
|
40
|
-
"postversion": "
|
|
40
|
+
"postversion": "bash scripts/postrelease.sh"
|
|
41
41
|
},
|
|
42
42
|
"keywords": [
|
|
43
43
|
"grepmax",
|