grepmax 0.22.0 → 0.23.0

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.
@@ -10,7 +10,7 @@
10
10
  "name": "grepmax",
11
11
  "source": "./plugins/grepmax",
12
12
  "description": "Semantic code search for Claude Code. Automatically indexes your project and provides intelligent search capabilities.",
13
- "version": "0.22.0",
13
+ "version": "0.23.0",
14
14
  "author": {
15
15
  "name": "Robert Owens",
16
16
  "email": "robowens@me.com"
package/README.md CHANGED
@@ -72,6 +72,7 @@ gmax log src/lib/auth.ts # Git commit history for a path or symb
72
72
  gmax test handleAuth # Find tests via reverse call graph
73
73
  gmax impact handleAuth # Dependents + affected tests
74
74
  gmax similar handleAuth # Find similar code patterns
75
+ gmax audit --top 10 # God nodes, hub files, file cycles, dead candidates
75
76
  gmax dead handleAuth # Unused-symbol check via call graph (DEAD / PUBLIC EXPORT / LIVE)
76
77
  gmax context "auth system" --budget 4000 # Token-budgeted topic summary
77
78
  gmax context src/lib/auth.ts --budget 4000 # Deterministic file/path context
@@ -132,7 +133,7 @@ Plugins auto-update when you run `npm install -g grepmax@latest` — no need to
132
133
  | `extract_symbol` | Complete function/class body by symbol name. |
133
134
  | `peek_symbol` | Compact overview: signature + callers + callees. |
134
135
  | `dead` | Unused-symbol check via call graph. Returns `DEAD`, `PUBLIC EXPORT`, or `LIVE` with caller count. |
135
- | `audit` | Graph summary: god nodes, hub files, and dead-code candidates in one call. |
136
+ | `audit` | Graph summary: god nodes, hub files, symbol-derived file dependency cycles, and dead-code candidates. |
136
137
  | `get_neighbors` | Graph primitive: symbols reachable from a node along call edges within N hops. |
137
138
  | `find_paths` | Graph primitive: shortest call-graph path between two symbols. |
138
139
  | `subgraph_for_files` | Graph primitive: local dependency subgraph for a set of files. |
@@ -64,6 +64,73 @@ const MIN_GOD_NAME_LEN = 3;
64
64
  function rel(p, prefix) {
65
65
  return p.startsWith(prefix) ? p.slice(prefix.length) : p;
66
66
  }
67
+ function findFileCycles(deps, prefix, top) {
68
+ const indexByFile = new Map();
69
+ const lowlink = new Map();
70
+ const stack = [];
71
+ const onStack = new Set();
72
+ const components = [];
73
+ let index = 0;
74
+ const strongConnect = (file) => {
75
+ var _a, _b, _c, _d, _e;
76
+ indexByFile.set(file, index);
77
+ lowlink.set(file, index);
78
+ index++;
79
+ stack.push(file);
80
+ onStack.add(file);
81
+ for (const dep of (_a = deps.get(file)) !== null && _a !== void 0 ? _a : []) {
82
+ if (!indexByFile.has(dep)) {
83
+ strongConnect(dep);
84
+ lowlink.set(file, Math.min((_b = lowlink.get(file)) !== null && _b !== void 0 ? _b : 0, (_c = lowlink.get(dep)) !== null && _c !== void 0 ? _c : 0));
85
+ }
86
+ else if (onStack.has(dep)) {
87
+ lowlink.set(file, Math.min((_d = lowlink.get(file)) !== null && _d !== void 0 ? _d : 0, (_e = indexByFile.get(dep)) !== null && _e !== void 0 ? _e : 0));
88
+ }
89
+ }
90
+ if (lowlink.get(file) !== indexByFile.get(file))
91
+ return;
92
+ const component = [];
93
+ while (stack.length > 0) {
94
+ const dep = stack.pop();
95
+ onStack.delete(dep);
96
+ component.push(dep);
97
+ if (dep === file)
98
+ break;
99
+ }
100
+ if (component.length > 1)
101
+ components.push(component);
102
+ };
103
+ const files = new Set();
104
+ for (const [file, targets] of deps) {
105
+ files.add(file);
106
+ for (const target of targets)
107
+ files.add(target);
108
+ }
109
+ for (const file of [...files].sort()) {
110
+ if (!indexByFile.has(file))
111
+ strongConnect(file);
112
+ }
113
+ return components
114
+ .map((component) => {
115
+ var _a;
116
+ const set = new Set(component);
117
+ let edgeCount = 0;
118
+ for (const file of component) {
119
+ for (const dep of (_a = deps.get(file)) !== null && _a !== void 0 ? _a : []) {
120
+ if (set.has(dep))
121
+ edgeCount++;
122
+ }
123
+ }
124
+ return {
125
+ files: component.map((f) => rel(f, prefix)).sort(),
126
+ edgeCount,
127
+ };
128
+ })
129
+ .sort((a, b) => b.files.length - a.files.length ||
130
+ b.edgeCount - a.edgeCount ||
131
+ a.files.join("\0").localeCompare(b.files.join("\0")))
132
+ .slice(0, top);
133
+ }
67
134
  /**
68
135
  * Pure aggregation over chunk rows — no DB, no I/O. Builds the symbol→def map,
69
136
  * cross-file inbound edges, and per-file fan-in/out, then derives god nodes
@@ -170,6 +237,23 @@ function computeAudit(rows, prefix, top) {
170
237
  });
171
238
  }
172
239
  hubFiles.sort((a, b) => b.dependents - a.dependents || b.defines - a.defines);
240
+ const fileDeps = new Map();
241
+ for (const [file, refs] of fileOutRefs) {
242
+ for (const s of refs) {
243
+ if ((0, callsites_1.isBuiltinCallee)(s))
244
+ continue;
245
+ const defFiles = defFileCounts.get(s);
246
+ if (!defFiles || defFiles.size !== 1)
247
+ continue;
248
+ const [depFile] = defFiles;
249
+ if (!depFile || depFile === file)
250
+ continue;
251
+ if (!fileDeps.has(file))
252
+ fileDeps.set(file, new Set());
253
+ fileDeps.get(file).add(depFile);
254
+ }
255
+ }
256
+ const fileCycles = findFileCycles(fileDeps, prefix, top);
173
257
  // Dead candidates — non-exported in-project symbols with zero inbound
174
258
  // references anywhere (including their own file).
175
259
  const deadAll = [];
@@ -186,10 +270,17 @@ function computeAudit(rows, prefix, top) {
186
270
  scannedFiles: files.size,
187
271
  godNodes: godNodes.slice(0, top),
188
272
  hubFiles: hubFiles.filter((h) => h.dependents > 0).slice(0, top),
273
+ fileCycles,
189
274
  deadCandidates: deadAll.slice(0, top),
190
275
  deadTotal: deadAll.length,
191
276
  };
192
277
  }
278
+ function formatCycle(files) {
279
+ const shown = files.length > 6
280
+ ? [...files.slice(0, 6), `... ${files.length - 6} more`]
281
+ : files;
282
+ return shown.join(", ");
283
+ }
193
284
  function formatHuman(r) {
194
285
  const out = [];
195
286
  out.push(`${style.bold("Audit")} ${style.dim(`— ${r.scannedChunks} chunks across ${r.scannedFiles} files`)}`);
@@ -217,6 +308,17 @@ function formatHuman(r) {
217
308
  }
218
309
  }
219
310
  out.push("");
311
+ out.push(style.bold("File dependency cycles") +
312
+ style.dim(" (symbol-derived, bounded SCCs)"));
313
+ if (r.fileCycles.length === 0) {
314
+ out.push(style.dim(" none"));
315
+ }
316
+ else {
317
+ for (const c of r.fileCycles) {
318
+ out.push(` ${formatCycle(c.files)} ${style.dim(`${c.files.length} files, ${c.edgeCount} internal edges`)}`);
319
+ }
320
+ }
321
+ out.push("");
220
322
  out.push(style.bold("Dead-code candidates") +
221
323
  style.dim(` (${r.deadTotal} non-exported symbols with zero inbound refs)`));
222
324
  if (r.deadCandidates.length === 0) {
@@ -246,6 +348,9 @@ function formatAgent(r) {
246
348
  for (const h of r.hubFiles) {
247
349
  lines.push(`hub\t${h.file}\t${h.dependents}\t${h.defines}\t${h.fanOut}`);
248
350
  }
351
+ for (const c of r.fileCycles) {
352
+ lines.push(`cycle\t${c.files.join(",")}\t${c.files.length}\t${c.edgeCount}`);
353
+ }
249
354
  for (const d of r.deadCandidates) {
250
355
  lines.push(`dead\t${d.symbol}\t${d.file}:${d.line + 1}`);
251
356
  }
@@ -253,11 +358,12 @@ function formatAgent(r) {
253
358
  return lines.join("\n");
254
359
  }
255
360
  exports.audit = new commander_1.Command("audit")
256
- .description("Graph-summary of the indexed project god nodes (most depended-upon " +
257
- "symbols), hub files (most depended-upon files), and dead-code candidates " +
258
- "(non-exported symbols with zero inbound references). One pass over the " +
259
- "static call graph; dynamic dispatch / reflection / eval are invisible, so " +
260
- "dead candidates are hypotheses, not proof.")
361
+ .description("Graph-summary of the indexed project: god nodes (most depended-upon " +
362
+ "symbols), hub files (most depended-upon files), symbol-derived file " +
363
+ "dependency cycles, and dead-code candidates (non-exported symbols with " +
364
+ "zero inbound references). One pass over the static reference graph; " +
365
+ "dynamic dispatch / reflection / eval are invisible, so dead candidates " +
366
+ "are hypotheses, not proof.")
261
367
  .option("--root <dir>", "Project root directory")
262
368
  .option("--in <subpath>", "Restrict to a sub-path of the project (repeatable)", (value, prev) => prev ? [...prev, value] : [value])
263
369
  .option("--exclude <subpath>", "Exclude a sub-path of the project (repeatable)", (value, prev) => prev ? [...prev, value] : [value])
@@ -1042,6 +1042,7 @@ exports.mcp = new commander_1.Command("mcp")
1042
1042
  "start_line",
1043
1043
  "defined_symbols",
1044
1044
  "referenced_symbols",
1045
+ "type_referenced_symbols",
1045
1046
  "is_exported",
1046
1047
  ])
1047
1048
  .where((0, filter_builder_1.pathStartsWith)(prefix))
@@ -1055,7 +1056,12 @@ exports.mcp = new commander_1.Command("mcp")
1055
1056
  start_line: Number(r.start_line || 0),
1056
1057
  is_exported: Boolean(r.is_exported),
1057
1058
  defined_symbols: toStringArray(r.defined_symbols),
1058
- referenced_symbols: toStringArray(r.referenced_symbols),
1059
+ referenced_symbols: [
1060
+ ...new Set([
1061
+ ...toStringArray(r.referenced_symbols),
1062
+ ...toStringArray(r.type_referenced_symbols),
1063
+ ]),
1064
+ ],
1059
1065
  })), prefix, top);
1060
1066
  const lines = [];
1061
1067
  lines.push(`Audit — ${audit.scannedChunks} chunks across ${audit.scannedFiles} files`);
@@ -1074,6 +1080,13 @@ exports.mcp = new commander_1.Command("mcp")
1074
1080
  if (audit.hubFiles.length === 0)
1075
1081
  lines.push(" none");
1076
1082
  lines.push("");
1083
+ lines.push("File dependency cycles (symbol-derived):");
1084
+ for (const c of audit.fileCycles) {
1085
+ lines.push(` ${c.files.join(", ")} - ${c.files.length} files, ${c.edgeCount} internal edges`);
1086
+ }
1087
+ if (audit.fileCycles.length === 0)
1088
+ lines.push(" none");
1089
+ lines.push("");
1077
1090
  lines.push(`Dead-code candidates (${audit.deadTotal} non-exported symbols with zero inbound refs):`);
1078
1091
  for (const d of audit.deadCandidates) {
1079
1092
  lines.push(` ${d.symbol} (${d.file}:${d.line + 1})`);
@@ -2204,7 +2217,7 @@ exports.mcp = new commander_1.Command("mcp")
2204
2217
  },
2205
2218
  }, handleDead);
2206
2219
  tool("audit", {
2207
- 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.",
2220
+ description: "Graph-summary of the indexed project in one call: god nodes (most depended-upon symbols), hub files (most depended-upon files), symbol-derived file dependency cycles, and dead-code candidates (non-exported symbols with zero inbound references). Built from the static reference graph; dynamic dispatch, reflection, and eval are invisible, so dead candidates are hypotheses; verify with the `dead` tool before acting.",
2208
2221
  inputSchema: {
2209
2222
  root: zod_1.z
2210
2223
  .string()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "grepmax",
3
- "version": "0.22.0",
3
+ "version": "0.23.0",
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
  "lint:fix": "biome check --write .",
38
38
  "typecheck": "tsc --noEmit",
39
39
  "prepublishOnly": "pnpm build",
40
- "preversion": "pnpm test && pnpm typecheck",
40
+ "preversion": "pnpm test && pnpm typecheck && pnpm run format:check",
41
41
  "version": "bash scripts/sync-versions.sh && git add -A",
42
42
  "postversion": "bash scripts/postrelease.sh"
43
43
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "grepmax",
3
- "version": "0.22.0",
3
+ "version": "0.23.0",
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",