grepmax 0.17.2 → 0.17.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -64,6 +64,7 @@ gmax log src/lib/auth.ts # Git commit history for a path or symb
64
64
  gmax test handleAuth # Find tests via reverse call graph
65
65
  gmax impact handleAuth # Dependents + affected tests
66
66
  gmax similar handleAuth # Find similar code patterns
67
+ gmax dead handleAuth # Unused-symbol check via call graph (DEAD / PUBLIC EXPORT / LIVE)
67
68
  gmax context "auth system" --budget 4000 # Token-budgeted topic summary
68
69
  ```
69
70
 
@@ -121,6 +122,7 @@ Plugins auto-update when you run `npm install -g grepmax@latest` — no need to
121
122
  | `trace_calls` | Call graph: importers, callers (multi-hop), callees with file:line. |
122
123
  | `extract_symbol` | Complete function/class body by symbol name. |
123
124
  | `peek_symbol` | Compact overview: signature + callers + callees. |
125
+ | `dead` | Unused-symbol check via call graph. Returns `DEAD`, `PUBLIC EXPORT`, or `LIVE` with caller count. |
124
126
  | `list_symbols` | Indexed symbols with role and export status. |
125
127
  | `index_status` | Index health: chunks, files, projects, watcher status. |
126
128
  | `summarize_project` | Project overview: languages, structure, key symbols, entry points. |
@@ -277,6 +279,7 @@ fixtures/
277
279
  | `GMAX_WORKER_THREADS` | Worker threads for embedding | 50% of cores |
278
280
  | `GMAX_DEBUG` | Debug logging | Off |
279
281
  | `GMAX_SUMMARIZER` | Enable summarizer auto-start (`1`) | Off |
282
+ | `GMAX_RERANK` | Enable ColBERT rerank (`1`) — off by default since v0.17.1 ([why](docs/known-limitations.md)) | Off |
280
283
 
281
284
  ## Troubleshooting
282
285
 
@@ -293,6 +296,20 @@ gmax watch stop && gmax watch --daemon -b # Restart daemon
293
296
 
294
297
  See [CLAUDE.md](CLAUDE.md) for development setup, commands, and architecture details.
295
298
 
299
+ ### Benchmarks
300
+
301
+ Two evaluation harnesses live in the repo. Both emit stable JSON via `:json` variants.
302
+
303
+ ```bash
304
+ pnpm bench:recall # 97-case internal eval against gmax's own repo
305
+ pnpm bench:recall:json
306
+ pnpm bench:oss # P1 definition-lookup across express, lodash, platform (sverklo-bench fixtures)
307
+ pnpm bench:oss:json
308
+ GMAX_EVAL_RERANK=1 pnpm bench:oss # toggle ColBERT rerank
309
+ ```
310
+
311
+ The OSS bench requires the fixture repos to be indexed first — see [`docs/known-limitations.md`](docs/known-limitations.md) for the most recent rerank-on-vs-off comparison across 4 datasets / 131 cases.
312
+
296
313
  ## Attribution
297
314
 
298
315
  grepmax is built upon the foundation of [mgrep](https://github.com/mixedbread-ai/mgrep) by MixedBread. See the [NOTICE](NOTICE) file for details.
@@ -44,18 +44,111 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
44
44
  Object.defineProperty(exports, "__esModule", { value: true });
45
45
  exports.context = void 0;
46
46
  const fs = __importStar(require("node:fs"));
47
+ const path = __importStar(require("node:path"));
47
48
  const commander_1 = require("commander");
48
49
  const searcher_1 = require("../lib/search/searcher");
49
50
  const skeleton_1 = require("../lib/skeleton");
50
51
  const vector_db_1 = require("../lib/store/vector-db");
51
- const filter_builder_1 = require("../lib/utils/filter-builder");
52
+ const arrow_1 = require("../lib/utils/arrow");
52
53
  const exit_1 = require("../lib/utils/exit");
54
+ const filter_builder_1 = require("../lib/utils/filter-builder");
53
55
  const project_registry_1 = require("../lib/utils/project-registry");
54
56
  const project_root_1 = require("../lib/utils/project-root");
55
- const arrow_1 = require("../lib/utils/arrow");
56
57
  function estimateTokens(text) {
57
58
  return Math.ceil(text.length / 4);
58
59
  }
60
+ function addSection(state, text, budget) {
61
+ const tokens = estimateTokens(text);
62
+ if (state.tokensUsed + tokens > budget)
63
+ return false;
64
+ state.sections.push(text);
65
+ state.tokensUsed += tokens;
66
+ return true;
67
+ }
68
+ function relPath(projectRoot, p) {
69
+ return p.startsWith(`${projectRoot}/`) ? p.slice(projectRoot.length + 1) : p;
70
+ }
71
+ function chunkPath(chunk) {
72
+ const metadata = chunk.metadata;
73
+ return String(chunk.path || (metadata === null || metadata === void 0 ? void 0 : metadata.path) || "");
74
+ }
75
+ function chunkStartLine(chunk) {
76
+ var _a, _b, _c, _d;
77
+ return Number((_d = (_b = (_a = chunk.start_line) !== null && _a !== void 0 ? _a : chunk.startLine) !== null && _b !== void 0 ? _b : (_c = chunk.generated_metadata) === null || _c === void 0 ? void 0 : _c.start_line) !== null && _d !== void 0 ? _d : 0);
78
+ }
79
+ function chunkEndLine(chunk) {
80
+ var _a, _b, _c, _d;
81
+ const start = chunkStartLine(chunk);
82
+ return Number((_d = (_b = (_a = chunk.end_line) !== null && _a !== void 0 ? _a : chunk.endLine) !== null && _b !== void 0 ? _b : (_c = chunk.generated_metadata) === null || _c === void 0 ? void 0 : _c.end_line) !== null && _d !== void 0 ? _d : start);
83
+ }
84
+ function resolveExistingPath(target, root, projectRoot) {
85
+ const candidates = [
86
+ path.isAbsolute(target) ? target : path.resolve(root, target),
87
+ path.resolve(projectRoot, target),
88
+ ];
89
+ for (const candidate of candidates) {
90
+ if (fs.existsSync(candidate))
91
+ return candidate;
92
+ }
93
+ return null;
94
+ }
95
+ function renderPathContext(target, absPath, projectRoot, budget) {
96
+ return __awaiter(this, void 0, void 0, function* () {
97
+ const state = {
98
+ sections: [],
99
+ tokensUsed: 0,
100
+ };
101
+ const header = `=== Context: "${target}" ===`;
102
+ addSection(state, header, budget);
103
+ const stat = fs.statSync(absPath);
104
+ const targetSection = [
105
+ "\n## Target",
106
+ `${relPath(projectRoot, absPath)} [${stat.isDirectory() ? "directory" : "file"}]`,
107
+ ].join("\n");
108
+ addSection(state, targetSection, budget);
109
+ if (stat.isDirectory()) {
110
+ const entries = fs
111
+ .readdirSync(absPath, { withFileTypes: true })
112
+ .filter((entry) => !entry.name.startsWith("."))
113
+ .sort((a, b) => a.name.localeCompare(b.name))
114
+ .slice(0, 40)
115
+ .map((entry) => `${entry.isDirectory() ? "dir " : "file"} ${entry.name}`);
116
+ if (entries.length > 0) {
117
+ addSection(state, ["\n## Directory Entries", ...entries].join("\n"), budget);
118
+ }
119
+ return state;
120
+ }
121
+ const content = fs.readFileSync(absPath, "utf-8");
122
+ const skeletonizer = new skeleton_1.Skeletonizer();
123
+ yield skeletonizer.init();
124
+ if (skeletonizer.isSupported(absPath).supported) {
125
+ try {
126
+ const result = yield skeletonizer.skeletonizeFile(absPath, content);
127
+ if (result.success) {
128
+ addSection(state, [
129
+ "\n## File Structure",
130
+ `--- ${relPath(projectRoot, absPath)} (skeleton, ~${result.tokenEstimate} tokens) ---`,
131
+ result.skeleton,
132
+ ].join("\n"), budget);
133
+ }
134
+ }
135
+ catch (_a) {
136
+ // Skeleton is a convenience in path mode; fall through to excerpt.
137
+ }
138
+ }
139
+ const lines = content.split("\n");
140
+ const excerptLines = lines.slice(0, Math.min(lines.length, 120));
141
+ const omitted = lines.length > excerptLines.length
142
+ ? `\n... (+${lines.length - excerptLines.length} more lines)`
143
+ : "";
144
+ addSection(state, [
145
+ "\n## File Excerpt",
146
+ `--- ${relPath(projectRoot, absPath)}:1 ---`,
147
+ `${excerptLines.join("\n")}${omitted}`,
148
+ ].join("\n"), budget);
149
+ return state;
150
+ });
151
+ }
59
152
  exports.context = new commander_1.Command("context")
60
153
  .description("Generate a token-budgeted topic summary (search + skeleton + extract)")
61
154
  .argument("<topic>", "Natural language topic or directory path")
@@ -64,7 +157,7 @@ exports.context = new commander_1.Command("context")
64
157
  .option("--root <dir>", "Project root directory")
65
158
  .option("--agent", "Compact output for AI agents", false)
66
159
  .action((topic, opts) => __awaiter(void 0, void 0, void 0, function* () {
67
- var _a, _b, _c, _d, _e, _f, _g, _h, _j;
160
+ var _a, _b, _c, _d, _e;
68
161
  const budget = Number.parseInt(opts.budget || "4000", 10) || 4000;
69
162
  const maxResults = Number.parseInt(opts.maxResults || "10", 10) || 10;
70
163
  let vectorDb = null;
@@ -75,8 +168,14 @@ exports.context = new commander_1.Command("context")
75
168
  const projectRoot = (_a = (0, project_root_1.findProjectRoot)(root)) !== null && _a !== void 0 ? _a : root;
76
169
  const paths = (0, project_root_1.ensureProjectPaths)(projectRoot);
77
170
  vectorDb = new vector_db_1.VectorDB(paths.lancedbDir);
171
+ const pathTarget = resolveExistingPath(topic, root, projectRoot);
172
+ if (pathTarget) {
173
+ const rendered = yield renderPathContext(topic, pathTarget, projectRoot, budget);
174
+ rendered.sections.push(`\n(~${rendered.tokensUsed}/${budget} tokens used)`);
175
+ console.log(rendered.sections.join("\n"));
176
+ return;
177
+ }
78
178
  const searcher = new searcher_1.Searcher(vectorDb);
79
- const rel = (p) => p.startsWith(`${projectRoot}/`) ? p.slice(projectRoot.length + 1) : p;
80
179
  // Phase 1: Semantic search
81
180
  const response = yield searcher.search(topic, maxResults, { rerank: true }, {}, projectRoot);
82
181
  if (response.data.length === 0) {
@@ -94,11 +193,11 @@ exports.context = new commander_1.Command("context")
94
193
  const entryPoints = orchestrators.length > 0 ? orchestrators : response.data.slice(0, 3);
95
194
  const epSection = ["\n## Entry Points"];
96
195
  for (const r of entryPoints.slice(0, 5)) {
97
- const p = String(r.path || ((_b = r.metadata) === null || _b === void 0 ? void 0 : _b.path) || "");
98
- const line = Number((_c = r.start_line) !== null && _c !== void 0 ? _c : 0);
99
- const sym = (_e = (_d = (0, arrow_1.toArr)(r.defined_symbols)) === null || _d === void 0 ? void 0 : _d[0]) !== null && _e !== void 0 ? _e : "";
196
+ const p = chunkPath(r);
197
+ const line = chunkStartLine(r);
198
+ const sym = (_c = (_b = (0, arrow_1.toArr)(r.defined_symbols)) === null || _b === void 0 ? void 0 : _b[0]) !== null && _c !== void 0 ? _c : "";
100
199
  const role = String(r.role || "IMPLEMENTATION");
101
- epSection.push(`${rel(p)}:${line + 1} ${sym} [${role}]`);
200
+ epSection.push(`${relPath(projectRoot, p)}:${line + 1} ${sym} [${role}]`);
102
201
  }
103
202
  const epText = epSection.join("\n");
104
203
  if (tokensUsed + estimateTokens(epText) <= budget) {
@@ -109,24 +208,24 @@ exports.context = new commander_1.Command("context")
109
208
  const topChunks = entryPoints.slice(0, 3);
110
209
  const bodySection = ["\n## Key Functions"];
111
210
  for (const r of topChunks) {
112
- const absP = String(r.path || "");
113
- const startLine = Number((_f = r.start_line) !== null && _f !== void 0 ? _f : 0);
114
- const endLine = Number((_g = r.end_line) !== null && _g !== void 0 ? _g : startLine);
115
- const sym = (_j = (_h = (0, arrow_1.toArr)(r.defined_symbols)) === null || _h === void 0 ? void 0 : _h[0]) !== null && _j !== void 0 ? _j : "";
211
+ const absP = chunkPath(r);
212
+ const startLine = chunkStartLine(r);
213
+ const endLine = chunkEndLine(r);
214
+ const sym = (_e = (_d = (0, arrow_1.toArr)(r.defined_symbols)) === null || _d === void 0 ? void 0 : _d[0]) !== null && _e !== void 0 ? _e : "";
116
215
  try {
117
216
  const content = fs.readFileSync(absP, "utf-8");
118
217
  const allLines = content.split("\n");
119
218
  const body = allLines
120
219
  .slice(startLine, Math.min(endLine + 1, allLines.length))
121
220
  .join("\n");
122
- const blob = `\n--- ${rel(absP)}:${startLine + 1} ${sym} ---\n${body}`;
221
+ const blob = `\n--- ${relPath(projectRoot, absP)}:${startLine + 1} ${sym} ---\n${body}`;
123
222
  const blobTokens = estimateTokens(blob);
124
223
  if (tokensUsed + blobTokens > budget)
125
224
  break;
126
225
  bodySection.push(blob);
127
226
  tokensUsed += blobTokens;
128
227
  }
129
- catch (_k) {
228
+ catch (_f) {
130
229
  // File not readable — skip
131
230
  }
132
231
  }
@@ -135,9 +234,7 @@ exports.context = new commander_1.Command("context")
135
234
  }
136
235
  // Phase 4: File skeletons for unique files
137
236
  const uniqueFiles = [
138
- ...new Set(response.data
139
- .map((r) => String(r.path || ""))
140
- .filter(Boolean)),
237
+ ...new Set(response.data.map((r) => chunkPath(r)).filter(Boolean)),
141
238
  ].slice(0, 5);
142
239
  const skelSection = ["\n## File Structure"];
143
240
  const skeletonizer = new skeleton_1.Skeletonizer();
@@ -150,14 +247,14 @@ exports.context = new commander_1.Command("context")
150
247
  const result = yield skeletonizer.skeletonizeFile(absP, content);
151
248
  if (!result.success)
152
249
  continue;
153
- const blob = `\n--- ${rel(absP)} (skeleton, ~${result.tokenEstimate} tokens) ---\n${result.skeleton}`;
250
+ const blob = `\n--- ${relPath(projectRoot, absP)} (skeleton, ~${result.tokenEstimate} tokens) ---\n${result.skeleton}`;
154
251
  const blobTokens = estimateTokens(blob);
155
252
  if (tokensUsed + blobTokens > budget)
156
253
  break;
157
254
  skelSection.push(blob);
158
255
  tokensUsed += blobTokens;
159
256
  }
160
- catch (_l) {
257
+ catch (_g) {
161
258
  // Skip unreadable files
162
259
  }
163
260
  }
@@ -195,7 +292,7 @@ exports.context = new commander_1.Command("context")
195
292
  if (topRelated.length > 0) {
196
293
  const relSection = ["\n## Related Files"];
197
294
  for (const [p, count] of topRelated) {
198
- relSection.push(`${rel(p)} — ${count} shared symbol${count > 1 ? "s" : ""}`);
295
+ relSection.push(`${relPath(projectRoot, p)} — ${count} shared symbol${count > 1 ? "s" : ""}`);
199
296
  }
200
297
  const relText = relSection.join("\n");
201
298
  if (tokensUsed + estimateTokens(relText) <= budget) {
@@ -218,7 +315,7 @@ exports.context = new commander_1.Command("context")
218
315
  try {
219
316
  yield vectorDb.close();
220
317
  }
221
- catch (_m) { }
318
+ catch (_h) { }
222
319
  }
223
320
  yield (0, exit_1.gracefulExit)();
224
321
  }