grepmax 0.17.9 → 0.17.11

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
+ }));
@@ -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,23 @@ 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
+ },
242
260
  {
243
261
  name: "list_symbols",
244
262
  description: "List indexed symbols with role and export status.",
@@ -1338,6 +1356,75 @@ exports.mcp = new commander_1.Command("mcp")
1338
1356
  }
1339
1357
  });
1340
1358
  }
1359
+ function handleAudit(args) {
1360
+ return __awaiter(this, void 0, void 0, function* () {
1361
+ ensureWatcher();
1362
+ const root = typeof args.root === "string" && args.root
1363
+ ? path.resolve(args.root)
1364
+ : projectRoot;
1365
+ const prefix = root.endsWith("/") ? root : `${root}/`;
1366
+ const top = Math.min(Math.max(Number(args.top) || 10, 1), 50);
1367
+ try {
1368
+ const db = getVectorDb();
1369
+ const table = yield db.ensureTable();
1370
+ const rows = yield table
1371
+ .query()
1372
+ .select([
1373
+ "path",
1374
+ "start_line",
1375
+ "defined_symbols",
1376
+ "referenced_symbols",
1377
+ "is_exported",
1378
+ ])
1379
+ .where(`path LIKE '${(0, filter_builder_1.escapeSqlString)(prefix)}%'`)
1380
+ .limit(500000)
1381
+ .toArray();
1382
+ if (rows.length === 0) {
1383
+ return ok(`No indexed data found for ${root}. Run: gmax index --path ${root}`);
1384
+ }
1385
+ const audit = (0, audit_1.computeAudit)(rows.map((r) => ({
1386
+ path: String(r.path || ""),
1387
+ start_line: Number(r.start_line || 0),
1388
+ is_exported: Boolean(r.is_exported),
1389
+ defined_symbols: toStringArray(r.defined_symbols),
1390
+ referenced_symbols: toStringArray(r.referenced_symbols),
1391
+ })), prefix, top);
1392
+ const lines = [];
1393
+ lines.push(`Audit — ${audit.scannedChunks} chunks across ${audit.scannedFiles} files`);
1394
+ lines.push("");
1395
+ lines.push("God nodes (most depended-upon symbols):");
1396
+ for (const g of audit.godNodes) {
1397
+ lines.push(` ${g.symbol} — ${g.inboundFiles} files, ${g.totalRefs} refs (${g.file}:${g.line + 1})`);
1398
+ }
1399
+ if (audit.godNodes.length === 0)
1400
+ lines.push(" none");
1401
+ lines.push("");
1402
+ lines.push("Hub files (most depended-upon files):");
1403
+ for (const h of audit.hubFiles) {
1404
+ lines.push(` ${h.file} — ${h.dependents} dependents, ${h.defines} defs, fan-out ${h.fanOut}`);
1405
+ }
1406
+ if (audit.hubFiles.length === 0)
1407
+ lines.push(" none");
1408
+ lines.push("");
1409
+ lines.push(`Dead-code candidates (${audit.deadTotal} non-exported symbols with zero inbound refs):`);
1410
+ for (const d of audit.deadCandidates) {
1411
+ lines.push(` ${d.symbol} (${d.file}:${d.line + 1})`);
1412
+ }
1413
+ if (audit.deadCandidates.length === 0)
1414
+ lines.push(" none");
1415
+ if (audit.deadTotal > audit.deadCandidates.length) {
1416
+ lines.push(` … and ${audit.deadTotal - audit.deadCandidates.length} more`);
1417
+ }
1418
+ lines.push("");
1419
+ 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.");
1420
+ return ok(lines.join("\n"));
1421
+ }
1422
+ catch (e) {
1423
+ const msg = e instanceof Error ? e.message : String(e);
1424
+ return err(`Audit failed: ${msg}`);
1425
+ }
1426
+ });
1427
+ }
1341
1428
  function handleListSymbols(args) {
1342
1429
  return __awaiter(this, void 0, void 0, function* () {
1343
1430
  ensureWatcher();
@@ -2080,6 +2167,9 @@ exports.mcp = new commander_1.Command("mcp")
2080
2167
  case "dead":
2081
2168
  result = yield handleDead(toolArgs);
2082
2169
  break;
2170
+ case "audit":
2171
+ result = yield handleAudit(toolArgs);
2172
+ break;
2083
2173
  case "list_symbols":
2084
2174
  result = yield handleListSymbols(toolArgs);
2085
2175
  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);
@@ -553,8 +553,78 @@ class TreeSitterChunker {
553
553
  referencedSymbols.push(name);
554
554
  }
555
555
  };
556
+ // Leaf identifier node types across grammars (a bare name with no
557
+ // named children — `ErrorCodes`, not `a.ErrorCodes`).
558
+ const LEAF_ID_TYPES = new Set([
559
+ "identifier",
560
+ "type_identifier",
561
+ "constant", // Ruby
562
+ "name", // PHP
563
+ "simple_identifier", // Kotlin, Swift
564
+ "property_identifier",
565
+ "field_identifier", // Go
566
+ ]);
567
+ const isLeafId = (n) => {
568
+ var _a, _b;
569
+ return !!n &&
570
+ LEAF_ID_TYPES.has(n.type) &&
571
+ ((_b = (_a = n.namedChildren) === null || _a === void 0 ? void 0 : _a.length) !== null && _b !== void 0 ? _b : 0) === 0;
572
+ };
573
+ const firstNamed = (n) => { var _a, _b; return (_b = ((_a = n.namedChildren) !== null && _a !== void 0 ? _a : [])[0]) !== null && _b !== void 0 ? _b : null; };
574
+ // Resolve the type name out of an instantiation node (`new Foo`,
575
+ // `Foo{}`, `new Foo()`), preferring an explicit `type` field and
576
+ // falling back to the first named child (qualified names reduce to
577
+ // their rightmost segment via simpleRefName).
578
+ const instantiatedTypeName = (n) => {
579
+ var _a;
580
+ const typed = (_a = (n.childForFieldName ? n.childForFieldName("type") : null)) !== null && _a !== void 0 ? _a : firstNamed(n);
581
+ if (!typed)
582
+ return null;
583
+ if (isLeafId(typed))
584
+ return typed.text;
585
+ return simpleRefName(typed);
586
+ };
587
+ // First `type_identifier` reachable directly or via a `user_type`
588
+ // wrapper — covers Java `instanceof_expression` and Kotlin/Swift
589
+ // `check_expression` (`x is T`).
590
+ const firstTypeIdent = (n) => {
591
+ var _a, _b;
592
+ for (const c of (_a = n.namedChildren) !== null && _a !== void 0 ? _a : []) {
593
+ if (c.type === "type_identifier")
594
+ return c.text;
595
+ if (c.type === "user_type") {
596
+ const t = ((_b = c.namedChildren) !== null && _b !== void 0 ? _b : []).find((x) => x.type === "type_identifier");
597
+ if (t === null || t === void 0 ? void 0 : t.text)
598
+ return t.text;
599
+ }
600
+ }
601
+ return null;
602
+ };
603
+ // Member / scope access node types, one per grammar. We capture the
604
+ // head (object/scope) only when it is a Capitalized leaf identifier,
605
+ // so `ErrorCodes.VALIDATION` / `ErrorCodes::NOT_FOUND` yield an edge
606
+ // to `ErrorCodes` while `this.x` / `req.body` / lowercase locals do not.
607
+ const MEMBER_ACCESS_TYPES = new Set([
608
+ "member_expression", // TS/JS
609
+ "attribute", // Python
610
+ "selector_expression", // Go
611
+ "field_access", // Java
612
+ "member_access_expression", // C#
613
+ "scoped_identifier", // Rust
614
+ "scope_resolution", // Ruby
615
+ "navigation_expression", // Kotlin, Swift
616
+ "field_expression", // Scala
617
+ "class_constant_access_expression", // PHP
618
+ ]);
619
+ // Instantiation node types whose first/`type` child names a type.
620
+ const INSTANTIATION_TYPES = new Set([
621
+ "object_creation_expression", // Java, C#, PHP
622
+ "composite_literal", // Go
623
+ "struct_expression", // Rust
624
+ "instance_expression", // Scala
625
+ ]);
556
626
  const extractRefs = (n) => {
557
- var _a, _b, _c, _d, _e, _f, _g, _h;
627
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j;
558
628
  // Handle JS/TS (call_expression), Python (call), Lua (function_call)
559
629
  if (n.type === "call_expression" ||
560
630
  n.type === "call" ||
@@ -617,19 +687,26 @@ class TreeSitterChunker {
617
687
  }
618
688
  }
619
689
  }
620
- // Identifier-as-value references (TS/JS): edges the call-expression
621
- // capture above misses. These feed the graph-walk consumers (PPR,
690
+ // Identifier-as-value references: edges the call-expression capture
691
+ // above misses. These feed the graph-walk consumers (PPR,
622
692
  // `gmax dead <ClassName>`, audit) that need class/enum references,
623
- // not just method-call names.
693
+ // not just method-call names. Node types are grammar-specific but
694
+ // each falls into one of three shapes — instantiation, type-test, or
695
+ // member/scope access — handled uniformly across the 14 grammars.
696
+ // Shape 1 — instantiation: `new ClassName(...)`, `ClassName{...}`.
624
697
  if (n.type === "new_expression") {
625
- // `new ClassName(...)` — constructor is always a type reference.
698
+ // TS/JS — constructor may be qualified (`ns.ClassName`).
626
699
  const ctor = n.childForFieldName
627
700
  ? n.childForFieldName("constructor")
628
701
  : null;
629
702
  addRef(simpleRefName(ctor));
630
703
  }
631
- else if (n.type === "binary_expression") {
632
- // `x instanceof ClassName` — the right operand is a type reference.
704
+ else if (INSTANTIATION_TYPES.has(n.type)) {
705
+ addRef(instantiatedTypeName(n));
706
+ }
707
+ // Shape 2 — type-test: `x instanceof T`, `x is T`.
708
+ if (n.type === "binary_expression") {
709
+ // TS/JS, PHP — instanceof is a binary operator.
633
710
  const op = n.childForFieldName
634
711
  ? n.childForFieldName("operator")
635
712
  : null;
@@ -640,19 +717,27 @@ class TreeSitterChunker {
640
717
  addRef(simpleRefName(right));
641
718
  }
642
719
  }
643
- else if (n.type === "member_expression") {
644
- // `ClassName.MEMBER` / `Enum.MEMBER` capture the object only when
645
- // it looks like a type/namespace (Capitalized identifier), so we
646
- // get `ErrorCodes` from `ErrorCodes.VALIDATION` without flooding
647
- // the graph with `this.x` / `req.body` / lowercase-local access.
648
- const obj = n.childForFieldName
649
- ? n.childForFieldName("object")
650
- : null;
651
- if (obj && obj.type === "identifier" && /^[A-Z]/.test(obj.text)) {
652
- addRef(obj.text);
720
+ else if (n.type === "instanceof_expression" || // Java
721
+ n.type === "check_expression" // Kotlin, Swift (`x is T`)
722
+ ) {
723
+ addRef(firstTypeIdent(n));
724
+ }
725
+ else if (n.type === "is_pattern_expression") {
726
+ // C# — `x is BeyondError`: the type sits in a constant_pattern.
727
+ const cp = ((_h = n.namedChildren) !== null && _h !== void 0 ? _h : []).find((c) => c.type === "constant_pattern");
728
+ const id = cp ? firstNamed(cp) : null;
729
+ if (isLeafId(id) && id && /^[A-Z]/.test(id.text))
730
+ addRef(id.text);
731
+ }
732
+ // Shape 3 — member / scope access: `ErrorCodes.MEMBER`,
733
+ // `ErrorCodes::MEMBER`. Capitalized head only (skip `this.x`).
734
+ if (MEMBER_ACCESS_TYPES.has(n.type)) {
735
+ const head = firstNamed(n);
736
+ if (head && isLeafId(head) && /^[A-Z]/.test(head.text)) {
737
+ addRef(head.text);
653
738
  }
654
739
  }
655
- for (const child of (_h = n.namedChildren) !== null && _h !== void 0 ? _h : []) {
740
+ for (const child of (_j = n.namedChildren) !== null && _j !== void 0 ? _j : []) {
656
741
  extractRefs(child);
657
742
  }
658
743
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "grepmax",
3
- "version": "0.17.9",
3
+ "version": "0.17.11",
4
4
  "author": "Robert Owens <78518764+reowens@users.noreply.github.com>",
5
5
  "homepage": "https://github.com/reowens/grepmax",
6
6
  "bugs": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "grepmax",
3
- "version": "0.17.9",
3
+ "version": "0.17.11",
4
4
  "description": "Semantic code search for Claude Code. Automatically indexes your project and provides intelligent search capabilities.",
5
5
  "author": {
6
6
  "name": "Robert Owens",