grepmax 0.26.48 → 0.26.50
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/.claude-plugin/marketplace.json +1 -1
- package/README.md +77 -40
- package/dist/commands/search-output.js +22 -9
- package/dist/commands/search-run.js +6 -2
- package/dist/commands/search.js +10 -1
- package/dist/commands/serve.js +14 -8
- package/dist/eval-oss.js +2 -1
- package/dist/eval-relevance.js +337 -0
- package/dist/eval.js +4 -3
- package/dist/lib/daemon/ipc-handler.js +12 -0
- package/dist/lib/daemon/search-handler.js +14 -2
- package/dist/lib/eval/relevance-baseline.js +174 -0
- package/dist/lib/search/diagnostics.js +95 -0
- package/dist/lib/search/per-file.js +24 -0
- package/dist/lib/search/searcher.js +72 -13
- package/dist/lib/utils/daemon-search.js +34 -0
- package/package.json +3 -2
- package/plugins/grepmax/.claude-plugin/plugin.json +1 -1
|
@@ -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.26.
|
|
13
|
+
"version": "0.26.50",
|
|
14
14
|
"author": {
|
|
15
15
|
"name": "Robert Owens",
|
|
16
16
|
"email": "robowens@me.com"
|
package/README.md
CHANGED
|
@@ -36,6 +36,8 @@ Natural-language search that works like `grep`. Fast, local, and built for codin
|
|
|
36
36
|
|
|
37
37
|
## Quick Start
|
|
38
38
|
|
|
39
|
+
Requires **Node.js 22.12.0 or newer**.
|
|
40
|
+
|
|
39
41
|
```bash
|
|
40
42
|
npm install -g grepmax # 1. Install
|
|
41
43
|
cd my-repo && gmax add # 2. Add + index
|
|
@@ -89,7 +91,7 @@ gmax status # All indexed projects + chunk counts
|
|
|
89
91
|
gmax status --json # The same plus daemon, settings and workers, as JSON
|
|
90
92
|
```
|
|
91
93
|
|
|
92
|
-
|
|
94
|
+
The recorded benchmark below showed about 20% fewer LLM tokens and a 30% speedup on its workload. Results depend on the repository, query and agent workflow.
|
|
93
95
|
|
|
94
96
|
<div align="center">
|
|
95
97
|
<img src="public/bench.png" alt="gmax benchmark" width="100%" style="border-radius: 8px; margin: 20px 0;" />
|
|
@@ -115,11 +117,11 @@ gmax plugin add droid # Factory Droid only
|
|
|
115
117
|
gmax plugin remove claude # Remove specific plugin
|
|
116
118
|
```
|
|
117
119
|
|
|
118
|
-
|
|
120
|
+
After upgrading with `npm install -g grepmax@latest`, run `gmax plugin update` (or `gmax plugin update <client>`) to refresh integrations. Package installation does not modify your agent configuration; its postinstall script only prints this reminder.
|
|
119
121
|
|
|
120
122
|
### How it works per client
|
|
121
123
|
|
|
122
|
-
- **Claude Code:** Plugin with hooks (SessionStart, CwdChanged, SubagentStart, PreToolUse). Model uses CLI via `Bash(gmax ... --agent)`.
|
|
124
|
+
- **Claude Code:** Plugin with hooks (SessionStart, SessionEnd, CwdChanged, SubagentStart, PreToolUse). Model uses CLI via `Bash(gmax ... --agent)`.
|
|
123
125
|
- **OpenCode:** Tool shim with dynamic SKILL + session plugin for daemon startup. Model calls gmax tool directly.
|
|
124
126
|
- **Codex:** MCP server registration + AGENTS.md skill instructions.
|
|
125
127
|
- **Factory Droid:** Skills + SessionStart/SessionEnd hooks for daemon lifecycle.
|
|
@@ -153,7 +155,7 @@ Clients requesting progress receive operation-stage messages with increasing sta
|
|
|
153
155
|
| `list_projects` | List every indexed project (name, root, status, chunks) to pick a search scope. |
|
|
154
156
|
| `index_status` | Index health: chunks, files, projects, watcher status. |
|
|
155
157
|
| `summarize_project` | Project overview: languages, structure, key symbols, entry points. |
|
|
156
|
-
| `summarize_directory` |
|
|
158
|
+
| `summarize_directory` | Compatibility entry; summarization is disabled and generates no summaries. |
|
|
157
159
|
| `related_files` | Dependencies and dependents by shared symbols. |
|
|
158
160
|
| `recent_changes` | Recently modified indexed files. |
|
|
159
161
|
| `diff_changes` | Search scoped to git changes. |
|
|
@@ -189,18 +191,6 @@ gmax "query" [options]
|
|
|
189
191
|
| `--seed-symbol <name>` | Bias results toward an identifier you're working with (repeatable). | — |
|
|
190
192
|
| `--skeleton` | Show file skeletons for top matches. | `false` |
|
|
191
193
|
| `--context-for-llm` | Full function bodies + imports per result. | `false` |
|
|
192
|
-
|
|
193
|
-
## Experimental Orientation
|
|
194
|
-
|
|
195
|
-
`gmax surprises --experimental` finds file pairs that are semantically similar but not already connected by the indexed static graph. Use it for architecture orientation, duplicate-logic sweeps, and cross-package drift checks, not as proof that two files are unrelated.
|
|
196
|
-
|
|
197
|
-
```bash
|
|
198
|
-
gmax surprises --experimental --agent
|
|
199
|
-
gmax surprises --experimental --in packages/app/src --dir-depth 4 --agent
|
|
200
|
-
gmax surprises --experimental --exclude generated --top 10
|
|
201
|
-
```
|
|
202
|
-
|
|
203
|
-
The CLI and MCP output include score, max similarity, pair count, representative symbols, directory buckets, top similarities, applied penalties, and `gmax skeleton` follow-up hints. On large monorepos, prefer `--in`/`--exclude`; the default scan is capped at 50,000 rows and user-provided `--max-rows` is capped at 100,000. If a narrow `--in` scope returns no findings, increase `--dir-depth` so subdirectories are compared inside the scope.
|
|
204
194
|
| `--budget <tokens>` | Cap output tokens (for `--context-for-llm`). | `8000` |
|
|
205
195
|
| `--explain` | Show scoring breakdown per result. | `false` |
|
|
206
196
|
| `--scores` | Show relevance scores. | `false` |
|
|
@@ -213,7 +203,21 @@ The CLI and MCP output include score, max similarity, pair count, representative
|
|
|
213
203
|
| `--all-projects` | Search every indexed project; results grouped by project. | `false` |
|
|
214
204
|
| `--projects <list>` | Search only these projects (comma-separated names). | — |
|
|
215
205
|
| `--exclude-projects <list>` | With `--all-projects`, skip these projects. | — |
|
|
216
|
-
| `--min-score <n>` | Minimum
|
|
206
|
+
| `--min-score <n>` | Minimum score relative to this query’s top match, not an absolute confidence threshold. | `0` |
|
|
207
|
+
|
|
208
|
+
v0.26.50 carries an explicit `--per-file` value into retrieval as well as formatting. Values must be positive safe integers. Omitted requests retain the searcher's configured `GMAX_MAX_PER_FILE` cap or three; larger explicit values still respect `-m` and the existing candidate/rerank bounds. Explicit overrides require a daemon advertising `capabilities.perFileSearch: 1`; older live daemons are refused with an update/restart hint, without opening a fallback reader. An incompatible optional local HTTP fast path is skipped. v0.26.49 and older releases do not support this retrieval override.
|
|
209
|
+
|
|
210
|
+
## Experimental Orientation
|
|
211
|
+
|
|
212
|
+
`gmax surprises --experimental` finds file pairs that are semantically similar but not already connected by the indexed static graph. Use it for architecture orientation, duplicate-logic sweeps, and cross-package drift checks, not as proof that two files are unrelated.
|
|
213
|
+
|
|
214
|
+
```bash
|
|
215
|
+
gmax surprises --experimental --agent
|
|
216
|
+
gmax surprises --experimental --in packages/app/src --dir-depth 4 --agent
|
|
217
|
+
gmax surprises --experimental --exclude generated --top 10
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
The CLI and MCP output include score, max similarity, pair count, representative symbols, directory buckets, top similarities, applied penalties, and `gmax skeleton` follow-up hints. On large monorepos, prefer `--in`/`--exclude`; the default scan is capped at 50,000 rows and user-provided `--max-rows` is capped at 100,000. If a narrow `--in` scope returns no findings, increase `--dir-depth` so subdirectories are compared inside the scope.
|
|
217
221
|
|
|
218
222
|
## Background Daemon
|
|
219
223
|
|
|
@@ -228,7 +232,7 @@ gmax watch restart # Stop it, wait for it to exit, start a fresh one
|
|
|
228
232
|
gmax status # See all projects + watcher status
|
|
229
233
|
```
|
|
230
234
|
|
|
231
|
-
|
|
235
|
+
Adding or indexing a project can start the daemon automatically. MCP acquires primary-project watch leases lazily when an index-reading tool is used; discovery and catalog listing start no background work. It shuts down after 4 hours of inactivity, and hands off to a fresh daemon after 24 hours or once its memory footprint passes 2.5 GB. File-change batches are processed on up to four worker processes by default, added only when work backs up, with one kept free for searches; LanceDB compaction runs after writes rather than on every maintenance tick.
|
|
232
236
|
|
|
233
237
|
`gmax watch status` reports failed files, native watcher recovery or polling, dropped-event counts, and the last complete filesystem reconciliation. Polling scans run every five minutes, so recent edits can lag. Search and MCP responses retain health warnings even after the file queue drains. A complete reconciliation means the filesystem scan completed; queued embedding work can still be pending.
|
|
234
238
|
|
|
@@ -277,14 +281,14 @@ On Linux the per-socket list does not exist; the only option is
|
|
|
277
281
|
|
|
278
282
|
gmax can use a local LLM (via llama-server) for agentic codebase investigation. This is entirely opt-in and disabled by default — gmax works fine without it.
|
|
279
283
|
|
|
280
|
-
> **Memory footprint.** `gmax llm start`, `investigate`,
|
|
284
|
+
> **Memory footprint.** `gmax llm start`, `investigate`, and `review` can load a multi-GB
|
|
281
285
|
> GGUF model (typical coding models run 16–21 GB). On a memory-constrained machine that can stall
|
|
282
286
|
> or hang the host. Start it deliberately, not as a reflex — and if you run coding agents against
|
|
283
287
|
> this repo, they should be told not to invoke these on their own initiative. The bundled skill
|
|
284
288
|
> already carries that instruction.
|
|
285
289
|
>
|
|
286
|
-
> Everything else in gmax — search, `trace`, `impact`, `context`, `--context-for-llm` —
|
|
287
|
-
> the
|
|
290
|
+
> Everything else in gmax — search, `trace`, `impact`, `context`, `--context-for-llm` — uses
|
|
291
|
+
> local embeddings and the index without a generative LLM.
|
|
288
292
|
|
|
289
293
|
```bash
|
|
290
294
|
gmax llm on # Enable LLM features (persists to config)
|
|
@@ -330,16 +334,23 @@ The hook sends an IPC message to the daemon and returns instantly — it never b
|
|
|
330
334
|
|
|
331
335
|
### LLM Configuration
|
|
332
336
|
|
|
337
|
+
Set `GMAX_LLM_MODEL` to a GGUF file on your machine before starting; the built-in fallback path is machine-specific and may not exist. The server accepts loopback addresses only.
|
|
338
|
+
|
|
333
339
|
| Variable | Description | Default |
|
|
334
340
|
| --- | --- | --- |
|
|
335
|
-
| `GMAX_LLM_MODEL` | Path to GGUF model file |
|
|
341
|
+
| `GMAX_LLM_MODEL` | Path to GGUF model file | Machine-specific fallback |
|
|
342
|
+
| `GMAX_LLM_HOST` | Loopback address (`localhost` maps to IPv4 loopback) | `127.0.0.1` |
|
|
336
343
|
| `GMAX_LLM_BINARY` | llama-server binary | `llama-server` |
|
|
337
344
|
| `GMAX_LLM_PORT` | Server port | `8079` |
|
|
338
345
|
| `GMAX_LLM_IDLE_TIMEOUT` | Minutes before auto-stop | `30` |
|
|
339
346
|
|
|
347
|
+
### Summary compatibility commands
|
|
348
|
+
|
|
349
|
+
The summarizer is decommissioned. `gmax summarize` and MCP `summarize_directory` remain compatibility entries backed by a no-op; they do not generate summaries. `GMAX_SUMMARIZER` does not enable them. Investigation and review use the separate optional LLM described above.
|
|
350
|
+
|
|
340
351
|
## Architecture
|
|
341
352
|
|
|
342
|
-
|
|
353
|
+
Default shared data lives in `~/.gmax/`; configured secondary stores retain their own project indexes:
|
|
343
354
|
- `lancedb/` — LanceDB vector store (centralized, all projects)
|
|
344
355
|
- `cache/meta.lmdb` — file metadata cache (hashes, mtimes)
|
|
345
356
|
- `cache/watchers.lmdb` — watcher/daemon registry (LMDB, crash-safe)
|
|
@@ -347,7 +358,9 @@ All data lives in `~/.gmax/`:
|
|
|
347
358
|
- `daemon.pid` — PID file for daemon dedup
|
|
348
359
|
- `logs/` — daemon and server logs (5MB rotation)
|
|
349
360
|
- `config.json` — global config (model tier, embed mode)
|
|
350
|
-
- `models/` — embedding models
|
|
361
|
+
- `models/` — ONNX embedding models
|
|
362
|
+
- `hf/` — pinned Hugging Face cache for MLX embedding models
|
|
363
|
+
- `watch-leases.json` — active session/CLI watch leases
|
|
351
364
|
- `grammars/` — Tree-sitter grammars
|
|
352
365
|
- `projects.json` — registry of indexed directories
|
|
353
366
|
|
|
@@ -378,22 +391,19 @@ gmax embeds with IBM's Granite r2 code-embedding models. Two tiers are available
|
|
|
378
391
|
| `small` (default) | `granite-embedding-small-english-r2` | 384 | 47M |
|
|
379
392
|
| `standard` | `granite-embedding-english-r2` | 768 | 149M |
|
|
380
393
|
|
|
381
|
-
**384d
|
|
382
|
-
|
|
394
|
+
**384d remains the default.** In the recorded 97-case model-tier comparison
|
|
395
|
+
on gmax's own repo, the larger 768d model scored **~10 points
|
|
383
396
|
*worse* on Recall@10** than 384d, consistently on both the MLX GPU and ONNX CPU
|
|
384
397
|
embedding paths, with and without ColBERT rerank:
|
|
385
398
|
|
|
386
|
-
| Model | Recall@10 | MRR
|
|
399
|
+
| Model | Recall@10 | Reported MRR |
|
|
387
400
|
| --- | --- | --- |
|
|
388
401
|
| `small` / 384d | **0.72** | **0.51** |
|
|
389
402
|
| `standard` / 768d | 0.62 | 0.44 |
|
|
390
403
|
|
|
391
|
-
<sub>gmax
|
|
392
|
-
path and the rerank-on path show the same ~0.10 Recall@10 gap, so it isn't a
|
|
393
|
-
quantization artifact — the bigger model just retrieves worse on code here.</sub>
|
|
404
|
+
<sub>Historical gmax-repo comparison: 97 cases, MLX GPU, rerank off. CPU/q4 and rerank-on comparisons showed a similar gap. These figures are not a current-release benchmark; see the evaluation caveats below.</sub>
|
|
394
405
|
|
|
395
|
-
|
|
396
|
-
worker RAM, and dense-vector storage while *lowering* recall on code search.
|
|
406
|
+
The larger tier performed worse on that fixture and doubles dense-vector width; compute and memory costs depend on the backend.
|
|
397
407
|
Unless you have a measured reason to switch (e.g. a recall complaint on a very
|
|
398
408
|
large repo — benchmark it first), stay on 384d.
|
|
399
409
|
|
|
@@ -438,11 +448,11 @@ fixtures/
|
|
|
438
448
|
| Variable | Description | Default |
|
|
439
449
|
| --- | --- | --- |
|
|
440
450
|
| `GMAX_EMBED_MODE` | Force `cpu` or `gpu` | Auto-detect |
|
|
441
|
-
| `GMAX_WORKER_THREADS` | Worker processes for embedding; overrides `gmax config --worker-threads` | `min(4, max(2, cores/2))` |
|
|
451
|
+
| `GMAX_WORKER_THREADS` | Worker processes for embedding; overrides `gmax config --worker-threads` | `min(cores, 4, max(2, floor(cores/2)))` |
|
|
442
452
|
| `GMAX_WORKER_RSS_RECYCLE_MB` | Recycle workers that remain above this RSS; `0` disables the check | `1536` |
|
|
443
453
|
| `GMAX_DEBUG` | Debug logging | Off |
|
|
444
|
-
| `
|
|
445
|
-
| `
|
|
454
|
+
| `GMAX_RERANK` | Force ColBERT rerank (`1`); concentrated candidates can also enable it automatically ([why](docs/known-limitations.md)) | Off |
|
|
455
|
+
| `GMAX_CONCENTRATION_THRESHOLD` | Top-ten candidate share in one file that enables ColBERT; set above `1` for a true rerank-off baseline | `0.7` |
|
|
446
456
|
| `GMAX_ANN` | Experimental IVF_FLAT vector index (`1`); leave off unless validating recall on your corpus | Off |
|
|
447
457
|
|
|
448
458
|
## Troubleshooting
|
|
@@ -466,17 +476,17 @@ Compaction pauses index writes, waits for pending writes to commit, and opens a
|
|
|
466
476
|
|
|
467
477
|
### Known issues
|
|
468
478
|
|
|
469
|
-
|
|
479
|
+
For detection and recovery guidance, check
|
|
470
480
|
[`docs/known-limitations.md`](docs/known-limitations.md) — it covers what each case means and
|
|
471
481
|
whether you need to act.
|
|
472
482
|
|
|
473
483
|
| You see | What it means |
|
|
474
484
|
|---|---|
|
|
475
|
-
| `Optimize panicked ... inverted/builder.rs` |
|
|
485
|
+
| `Optimize panicked ... inverted/builder.rs` | The historical FTS merge defect is fixed in the shipped LanceDB 0.38.0 runtime. If it recurs on a current release, preserve the version and logs and report a regression; the rebuild guard remains a recovery tripwire. |
|
|
476
486
|
| `disabling auto-rebuild until an optimize succeeds` | Transient, not a wedge. The next successful optimize clears it. Search stays available. |
|
|
477
487
|
| `ANN: vector index not built` | Normal — exact search is the default. |
|
|
478
488
|
| `cannot reach the daemon socket from this sandbox` (exit 2) | The shell is sandboxed. Add the two keys in [Running under the Claude Code sandbox](#running-under-the-claude-code-sandbox); `gmax doctor` warns about the same gap. |
|
|
479
|
-
| `npm audit` reports advisories on install |
|
|
489
|
+
| `npm audit` reports advisories on install | v0.26.49 passed production and fresh packed-consumer audits. Audit findings can change: record the installed version and dependency path and report new findings; do not assume historical advisories still apply. |
|
|
480
490
|
|
|
481
491
|
## Contributing
|
|
482
492
|
|
|
@@ -484,7 +494,7 @@ See [CLAUDE.md](CLAUDE.md) for development setup, commands, and architecture det
|
|
|
484
494
|
|
|
485
495
|
### Benchmarks
|
|
486
496
|
|
|
487
|
-
Two evaluation harnesses
|
|
497
|
+
Two retrieval evaluation harnesses emit JSON via `:json` variants. Additional token and experimental-orientation harnesses are available as `bench:tokens` and `bench:surprises`.
|
|
488
498
|
|
|
489
499
|
```bash
|
|
490
500
|
pnpm bench:recall # 97-case internal eval against gmax's own repo
|
|
@@ -494,10 +504,37 @@ pnpm bench:oss:json
|
|
|
494
504
|
GMAX_EVAL_RERANK=1 pnpm bench:oss # toggle ColBERT rerank
|
|
495
505
|
```
|
|
496
506
|
|
|
497
|
-
The OSS
|
|
507
|
+
The OSS harness expects indexed express, lodash and platform fixtures at the paths defined in [src/eval-oss.ts](src/eval-oss.ts). The historical comparison in [known limitations](docs/known-limitations.md#colbert-rerank-is-opt-in-shape-sensitive-helps-monolithic-files-hurts-modular-repos) combines these with the internal fixture: four datasets / 131 cases. It is not a current release acceptance run.
|
|
508
|
+
|
|
509
|
+
Both retrieval harnesses request 20 results for diagnostics, but `mrrAt10` and Recall@10 credit only ranks 1–10. Later matches retain their actual ranks and count as found/hits, with zero reciprocal-rank credit. This cutoff correction shipped in v0.26.49; historical MRR figures recorded before the correction may include ranks 11–20 and require a new run or rescoring before comparison. Also set `GMAX_CONCENTRATION_THRESHOLD=2` for a true rerank-off comparison: `GMAX_EVAL_RERANK=0` alone does not disable automatic concentration gating. Freeze fixtures and record source, embedding/index identity and ranking configuration before drawing conclusions.
|
|
498
510
|
|
|
499
511
|
`pnpm bench:recall` also drives the model-tier comparison behind the 384d default — see [Model Tier](#model-tier) for why the larger 768d model is *not* the default.
|
|
500
512
|
|
|
513
|
+
#### Frozen multi-repository baseline
|
|
514
|
+
|
|
515
|
+
`bench:relevance` measures reviewed local fixtures through an already-ready daemon. It does not autostart a daemon or open a fallback search store. Keep private queries, source evidence and results under the ignored `docs/measurements/` directory. Review and freeze the fixture before seeing rankings; use a new fixture version when changing ground truth.
|
|
516
|
+
|
|
517
|
+
```bash
|
|
518
|
+
shasum -a 256 docs/measurements/relevance/fixture-v1.json
|
|
519
|
+
pnpm bench:relevance --fixture docs/measurements/relevance/fixture-v1.json \
|
|
520
|
+
--sha256 <reviewed-fixture-digest> \
|
|
521
|
+
--output docs/measurements/relevance/baseline-v1.json --repeats 2
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
The fixture schema is in [relevance-baseline.ts](src/lib/eval/relevance-baseline.ts). Its JSON contains `schemaVersion: 1`, `createdAt`, `purpose`, `corpora` and `cases`. Each corpus has an `id`, `root` (absolute or relative to the fixture) and `language`. Each case declares an `id`, `corpus`, `split` (`dev` or `heldout`), `origin` (`curated-source` or `observed-session`), `intent`, `query` and one or more `expected` targets. Targets require an exact relative `file`, definition `symbol`, one-based `startLine`/`endLine` and the SHA-256 of the source file.
|
|
525
|
+
|
|
526
|
+
A hit requires the exact file and either the definition symbol or overlap with the declared line range. The source runner also accepts target `match: "range"` to require range overlap without a symbol shortcut; omitted or `"symbol-or-range"` preserves legacy matching. Range overlap measures useful context discovery, not complete answer coverage. Choose answer-bearing ranges and reviewed alternatives: a declaration-only target can miss a useful function-body chunk. The October 4 baseline intentionally preserves its frozen declaration targets, so its 57.5% Recall@10 / 0.3275 MRR@10 describes target retrieval, not general answer quality. It covers 40 curated cases across TypeScript, JavaScript, Python and Swift; both repetitions produced identical ranks. These are not observed user-session misses or a ranking acceptance gate. Previously inspected cases must be treated as exposed development cases in successor measurements.
|
|
527
|
+
|
|
528
|
+
The report records source/index hashes, registry embedding identity, daemon snapshots, exclusions, per-case ranks, aggregate metrics and ordinary-session latency. A sibling `.samples.jsonl` checkpoints each completed request; the final JSON applies run-wide exclusions and is authoritative. Changed source/cache hashes, missing targets, unsettled indexes, search warnings and daemon replacement invalidate samples. The runner refuses to overwrite artifacts and exits 2 when any samples are excluded.
|
|
529
|
+
|
|
530
|
+
Requests use `rerank: false`, but the daemon's concentration gate can still enable reranking. Client environment settings cannot change an existing daemon's settings, and `scoreBreakdown.rerank` also holds the fallback base score when reranking is off.
|
|
531
|
+
|
|
532
|
+
v0.26.49 supports `--diagnostics`. This requires a ready daemon advertising `capabilities.searchDiagnostics: 1`; older daemons without that capability require an update. The evaluator refuses incompatible daemons before measuring and excludes responses missing requested diagnostics. Each sample records FTS availability/failure, effective settings and allowlisted daemon ranking environment, concentration-gate evaluation/activation and whether reranking was actually invoked.
|
|
533
|
+
|
|
534
|
+
The candidate trace contains pointers and stage ranks for at most the first 200 post-seed fusion candidates, plus full stage counts and an explicit truncation flag. Ranks cover vector/FTS, RRF, seeded fusion, stage-one selection, pooled filtering, selected rerank batch, final scoring, overlap deduplication and final display. Rerank-batch membership does not imply execution; use `gate.rerankInvoked`. Zero at a later stage means a traced candidate was removed; a target missing from a truncated trace may lie outside the reported head. Diagnostics add no database reads and preserve ranking/results. The frozen October 4 artifacts retain their original unknowns. Review ground truth and collect new diagnostic artifacts before tuning.
|
|
535
|
+
|
|
536
|
+
Each traced candidate also has an `outcome`: returned, stage-one/pooled cut, deduplicated, per-file limit or display limit. Display limit means the production loop filled its result window before examining that candidate; it does not assert that the candidate would pass the per-file cap.
|
|
537
|
+
|
|
501
538
|
## Attribution
|
|
502
539
|
|
|
503
540
|
grepmax is built upon the foundation of [mgrep](https://github.com/mixedbread-ai/mgrep) by MixedBread. See the [NOTICE](NOTICE) file for details.
|
|
@@ -48,6 +48,7 @@ const path = __importStar(require("node:path"));
|
|
|
48
48
|
const agent_search_formatter_1 = require("../lib/output/agent-search-formatter");
|
|
49
49
|
const compact_results_1 = require("../lib/output/compact-results");
|
|
50
50
|
const index_state_footer_1 = require("../lib/output/index-state-footer");
|
|
51
|
+
const per_file_1 = require("../lib/search/per-file");
|
|
51
52
|
const cross_project_1 = require("../lib/utils/cross-project");
|
|
52
53
|
const formatter_1 = require("../lib/utils/formatter");
|
|
53
54
|
const import_extractor_1 = require("../lib/utils/import-extractor");
|
|
@@ -69,14 +70,26 @@ const search_skeletons_1 = require("./search-skeletons");
|
|
|
69
70
|
*/
|
|
70
71
|
function executeServerSearch(params) {
|
|
71
72
|
return __awaiter(this, void 0, void 0, function* () {
|
|
73
|
+
var _a;
|
|
74
|
+
var _b;
|
|
72
75
|
const { server, pattern, exec_path, projectRootForServer, options, minScore, } = params;
|
|
76
|
+
const maxPerFile = (0, per_file_1.parseCliPerFile)(options.perFile);
|
|
73
77
|
try {
|
|
78
|
+
if (maxPerFile !== undefined) {
|
|
79
|
+
const health = yield fetch(`http://127.0.0.1:${server.port}/health`);
|
|
80
|
+
if (!health.ok)
|
|
81
|
+
return false;
|
|
82
|
+
const info = (yield health.json());
|
|
83
|
+
if (((_a = info.capabilities) === null || _a === void 0 ? void 0 : _a.perFileSearch) !== 1)
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
74
86
|
const response = yield fetch(`http://127.0.0.1:${server.port}/search`, {
|
|
75
87
|
method: "POST",
|
|
76
88
|
headers: { "Content-Type": "application/json" },
|
|
77
89
|
body: JSON.stringify({
|
|
78
90
|
query: pattern,
|
|
79
91
|
limit: parseInt(options.m, 10),
|
|
92
|
+
maxPerFile,
|
|
80
93
|
path: exec_path
|
|
81
94
|
? path.relative(projectRootForServer, path.resolve(exec_path))
|
|
82
95
|
: undefined,
|
|
@@ -161,7 +174,7 @@ function executeServerSearch(params) {
|
|
|
161
174
|
isPlain: true,
|
|
162
175
|
compact: options.compact,
|
|
163
176
|
content: options.content,
|
|
164
|
-
perFile: parseInt(options.perFile, 10),
|
|
177
|
+
perFile: parseInt((_b = options.perFile) !== null && _b !== void 0 ? _b : "3", 10),
|
|
165
178
|
showScores: options.scores,
|
|
166
179
|
});
|
|
167
180
|
console.log(output);
|
|
@@ -195,7 +208,7 @@ function executeServerSearch(params) {
|
|
|
195
208
|
function renderSearchOutput(params) {
|
|
196
209
|
return __awaiter(this, void 0, void 0, function* () {
|
|
197
210
|
var _a, _b, _c;
|
|
198
|
-
var _d, _e, _f, _g;
|
|
211
|
+
var _d, _e, _f, _g, _h, _j;
|
|
199
212
|
const { searchResult, options, minScore, crossProject, pattern, effectiveRoot, projectRoot, vectorDb, precomputedSkeletons, precomputedGraph, indexState, } = params;
|
|
200
213
|
if (!options.agent && ((_a = searchResult.warnings) === null || _a === void 0 ? void 0 : _a.length)) {
|
|
201
214
|
for (const w of searchResult.warnings) {
|
|
@@ -220,7 +233,7 @@ function renderSearchOutput(params) {
|
|
|
220
233
|
return defs.some((d) => regex.test(d));
|
|
221
234
|
});
|
|
222
235
|
}
|
|
223
|
-
catch (
|
|
236
|
+
catch (_k) {
|
|
224
237
|
// Invalid regex — skip
|
|
225
238
|
}
|
|
226
239
|
}
|
|
@@ -293,7 +306,7 @@ function renderSearchOutput(params) {
|
|
|
293
306
|
isPlain: true,
|
|
294
307
|
compact: options.compact,
|
|
295
308
|
content: options.content,
|
|
296
|
-
perFile: parseInt(options.perFile, 10),
|
|
309
|
+
perFile: parseInt((_d = options.perFile) !== null && _d !== void 0 ? _d : "3", 10),
|
|
297
310
|
showScores: options.scores,
|
|
298
311
|
});
|
|
299
312
|
}
|
|
@@ -355,7 +368,7 @@ function renderSearchOutput(params) {
|
|
|
355
368
|
}
|
|
356
369
|
}
|
|
357
370
|
}
|
|
358
|
-
catch (
|
|
371
|
+
catch (_l) { }
|
|
359
372
|
}
|
|
360
373
|
// Partial-index footer last, so it's the final line the agent reads —
|
|
361
374
|
// and emitted even on "(none)", where an empty result may just mean the
|
|
@@ -453,7 +466,7 @@ function renderSearchOutput(params) {
|
|
|
453
466
|
if (options.imports) {
|
|
454
467
|
const seenFiles = new Set();
|
|
455
468
|
for (const r of filteredData) {
|
|
456
|
-
const absP = (
|
|
469
|
+
const absP = (_f = (_e = r.path) !== null && _e !== void 0 ? _e : (_b = r.metadata) === null || _b === void 0 ? void 0 : _b.path) !== null && _f !== void 0 ? _f : "";
|
|
457
470
|
if (absP && !seenFiles.has(absP)) {
|
|
458
471
|
seenFiles.add(absP);
|
|
459
472
|
const imports = getImportsForFile(absP);
|
|
@@ -472,7 +485,7 @@ function renderSearchOutput(params) {
|
|
|
472
485
|
isPlain: true,
|
|
473
486
|
compact: options.compact,
|
|
474
487
|
content: options.content,
|
|
475
|
-
perFile: parseInt(options.perFile, 10),
|
|
488
|
+
perFile: parseInt((_g = options.perFile) !== null && _g !== void 0 ? _g : "3", 10),
|
|
476
489
|
showScores: options.scores,
|
|
477
490
|
});
|
|
478
491
|
console.log(output);
|
|
@@ -480,7 +493,7 @@ function renderSearchOutput(params) {
|
|
|
480
493
|
for (const r of filteredData) {
|
|
481
494
|
const b = r.scoreBreakdown;
|
|
482
495
|
if (b) {
|
|
483
|
-
const absP = (
|
|
496
|
+
const absP = (_j = (_h = r.path) !== null && _h !== void 0 ? _h : (_c = r.metadata) === null || _c === void 0 ? void 0 : _c.path) !== null && _j !== void 0 ? _j : "";
|
|
484
497
|
const relPath = absP.startsWith(projectRoot)
|
|
485
498
|
? absP.slice(projectRoot.length + 1)
|
|
486
499
|
: absP;
|
|
@@ -542,7 +555,7 @@ function renderSearchOutput(params) {
|
|
|
542
555
|
console.log(lines.join("\n"));
|
|
543
556
|
}
|
|
544
557
|
}
|
|
545
|
-
catch (
|
|
558
|
+
catch (_m) {
|
|
546
559
|
// Trace failed — skip silently
|
|
547
560
|
}
|
|
548
561
|
}
|
|
@@ -51,6 +51,7 @@ const grammar_loader_1 = require("../lib/index/grammar-loader");
|
|
|
51
51
|
const index_config_1 = require("../lib/index/index-config");
|
|
52
52
|
const sync_helpers_1 = require("../lib/index/sync-helpers");
|
|
53
53
|
const syncer_1 = require("../lib/index/syncer");
|
|
54
|
+
const per_file_1 = require("../lib/search/per-file");
|
|
54
55
|
const searcher_1 = require("../lib/search/searcher");
|
|
55
56
|
const vector_db_1 = require("../lib/store/vector-db");
|
|
56
57
|
const lock_1 = require("../lib/utils/lock");
|
|
@@ -76,6 +77,7 @@ function runSearch(params) {
|
|
|
76
77
|
var _a, _b;
|
|
77
78
|
var _c;
|
|
78
79
|
const { pattern, options, projectRoot, effectiveRoot, paths, projectStatus, searchFilters, pathFilter, seeds, } = params;
|
|
80
|
+
const maxPerFile = (0, per_file_1.parseCliPerFile)(options.perFile);
|
|
79
81
|
// Tracks a DB opened by the in-process path so it can be (a) returned to the
|
|
80
82
|
// caller for closing after render, or (b) closed here if indexing throws.
|
|
81
83
|
let openedDb = null;
|
|
@@ -96,13 +98,14 @@ function runSearch(params) {
|
|
|
96
98
|
let indexState;
|
|
97
99
|
if (!options.sync && !options.dryRun) {
|
|
98
100
|
try {
|
|
99
|
-
const {
|
|
101
|
+
const { sendSearchCommand } = yield Promise.resolve().then(() => __importStar(require("../lib/utils/daemon-search")));
|
|
100
102
|
const crossProject = Array.isArray(searchFilters.projectRoots);
|
|
101
|
-
const resp = yield
|
|
103
|
+
const resp = yield sendSearchCommand({
|
|
102
104
|
cmd: crossProject ? "search-v2" : "search",
|
|
103
105
|
projectRoot: effectiveRoot,
|
|
104
106
|
query: pattern,
|
|
105
107
|
limit: parseInt(options.m, 10),
|
|
108
|
+
maxPerFile,
|
|
106
109
|
filters: Object.keys(searchFilters).length > 0 ? searchFilters : undefined,
|
|
107
110
|
pathPrefix: pathFilter,
|
|
108
111
|
rerank: process.env.GMAX_RERANK === "1",
|
|
@@ -253,6 +256,7 @@ function runSearch(params) {
|
|
|
253
256
|
const searcher = new searcher_1.Searcher(vectorDb);
|
|
254
257
|
searchResult = yield searcher.search(pattern, parseInt(options.m, 10), {
|
|
255
258
|
rerank: process.env.GMAX_RERANK === "1",
|
|
259
|
+
maxPerFile,
|
|
256
260
|
explain: options.explain,
|
|
257
261
|
seeds,
|
|
258
262
|
}, Object.keys(searchFilters).length > 0
|
package/dist/commands/search.js
CHANGED
|
@@ -45,6 +45,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
45
45
|
exports.search = void 0;
|
|
46
46
|
const path = __importStar(require("node:path"));
|
|
47
47
|
const commander_1 = require("commander");
|
|
48
|
+
const per_file_1 = require("../lib/search/per-file");
|
|
48
49
|
const setup_helpers_1 = require("../lib/setup/setup-helpers");
|
|
49
50
|
const cross_project_1 = require("../lib/utils/cross-project");
|
|
50
51
|
const exit_1 = require("../lib/utils/exit");
|
|
@@ -61,7 +62,7 @@ exports.search = new commander_1.Command("search")
|
|
|
61
62
|
.description("Search code by meaning (default command)")
|
|
62
63
|
.option("-m <max_count>, --max-count <max_count>", "The maximum number of results to return (total)", "5")
|
|
63
64
|
.option("-c, --content", "Show full chunk content instead of snippets", false)
|
|
64
|
-
.option("--per-file <n>", "
|
|
65
|
+
.option("--per-file <n>", "Maximum retrieved matches per file (default: configured cap or 3)")
|
|
65
66
|
.option("--scores", "Show relevance scores", false)
|
|
66
67
|
.option("--explain", "Show scoring breakdown per result", false)
|
|
67
68
|
.option("--context-for-llm", "Return full function body + imports per result", false)
|
|
@@ -105,6 +106,14 @@ Examples:
|
|
|
105
106
|
var _a, _b, _c;
|
|
106
107
|
var _d, _e, _f, _g;
|
|
107
108
|
const options = cmd.optsWithGlobals();
|
|
109
|
+
try {
|
|
110
|
+
(0, per_file_1.parseCliPerFile)(options.perFile);
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
console.error(error.message);
|
|
114
|
+
process.exitCode = 1;
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
108
117
|
const root = process.cwd();
|
|
109
118
|
const minScore = Number.isFinite(Number.parseFloat(options.minScore))
|
|
110
119
|
? Number.parseFloat(options.minScore)
|
package/dist/commands/serve.js
CHANGED
|
@@ -64,8 +64,10 @@ const http = __importStar(require("node:http"));
|
|
|
64
64
|
const path = __importStar(require("node:path"));
|
|
65
65
|
const commander_1 = require("commander");
|
|
66
66
|
const config_1 = require("../config");
|
|
67
|
+
const per_file_1 = require("../lib/search/per-file");
|
|
67
68
|
const blocked_roots_1 = require("../lib/utils/blocked-roots");
|
|
68
69
|
const daemon_client_1 = require("../lib/utils/daemon-client");
|
|
70
|
+
const daemon_search_1 = require("../lib/utils/daemon-search");
|
|
69
71
|
const exit_1 = require("../lib/utils/exit");
|
|
70
72
|
const log_rotate_1 = require("../lib/utils/log-rotate");
|
|
71
73
|
const path_containment_1 = require("../lib/utils/path-containment");
|
|
@@ -194,7 +196,7 @@ function createServeHttpServer(projectRoot, deps) {
|
|
|
194
196
|
var _b, _c, _d;
|
|
195
197
|
(_a = deps.onActivity) === null || _a === void 0 ? void 0 : _a.call(deps);
|
|
196
198
|
if (req.method === "GET" && req.url === "/health") {
|
|
197
|
-
writeJson(res, 200, { status: "ok" });
|
|
199
|
+
writeJson(res, 200, { status: "ok", capabilities: { perFileSearch: 1 } });
|
|
198
200
|
return;
|
|
199
201
|
}
|
|
200
202
|
const ac = new AbortController();
|
|
@@ -223,6 +225,13 @@ function createServeHttpServer(projectRoot, deps) {
|
|
|
223
225
|
const query = typeof body.query === "string" ? body.query.trim() : "";
|
|
224
226
|
if (!query)
|
|
225
227
|
throw new HttpRequestError(400, "invalid_query");
|
|
228
|
+
let maxPerFile;
|
|
229
|
+
try {
|
|
230
|
+
maxPerFile = (0, per_file_1.validateMaxPerFile)(body.maxPerFile);
|
|
231
|
+
}
|
|
232
|
+
catch (_e) {
|
|
233
|
+
throw new HttpRequestError(400, "invalid_maxPerFile");
|
|
234
|
+
}
|
|
226
235
|
const limit = body.limit === undefined ? 10 : body.limit;
|
|
227
236
|
if (typeof limit !== "number" ||
|
|
228
237
|
!Number.isInteger(limit) ||
|
|
@@ -262,12 +271,8 @@ function createServeHttpServer(projectRoot, deps) {
|
|
|
262
271
|
if (scope.excludePrefixes.length > 0) {
|
|
263
272
|
filters.excludePrefixes = scope.excludePrefixes;
|
|
264
273
|
}
|
|
265
|
-
const response = yield deps.search({
|
|
266
|
-
|
|
267
|
-
limit,
|
|
268
|
-
pathPrefix: scope.pathPrefix,
|
|
269
|
-
filters: Object.keys(filters).length ? filters : undefined,
|
|
270
|
-
}, ac.signal);
|
|
274
|
+
const response = yield deps.search(Object.assign(Object.assign({ query,
|
|
275
|
+
limit }, (maxPerFile === undefined ? {} : { maxPerFile })), { pathPrefix: scope.pathPrefix, filters: Object.keys(filters).length ? filters : undefined }), ac.signal);
|
|
271
276
|
if (!response.ok) {
|
|
272
277
|
writeJson(res, daemonErrorStatus(response.error), {
|
|
273
278
|
error: (_c = response.error) !== null && _c !== void 0 ? _c : "search_failed",
|
|
@@ -453,11 +458,12 @@ exports.serve = new commander_1.Command("serve")
|
|
|
453
458
|
onActivity: () => {
|
|
454
459
|
lastActivity = Date.now();
|
|
455
460
|
},
|
|
456
|
-
search: (input, signal) => (0,
|
|
461
|
+
search: (input, signal) => (0, daemon_search_1.sendSearchCommand)({
|
|
457
462
|
cmd: "search",
|
|
458
463
|
projectRoot,
|
|
459
464
|
query: input.query,
|
|
460
465
|
limit: input.limit,
|
|
466
|
+
maxPerFile: input.maxPerFile,
|
|
461
467
|
pathPrefix: input.pathPrefix,
|
|
462
468
|
filters: input.filters,
|
|
463
469
|
rerank: true,
|
package/dist/eval-oss.js
CHANGED
|
@@ -57,6 +57,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
57
57
|
var _a, _b, _c, _d;
|
|
58
58
|
var _e;
|
|
59
59
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
60
|
+
exports.evaluateOss = evaluateOss;
|
|
60
61
|
// Same precaution as src/eval.ts — pin worker pool to 1 to avoid ONNX
|
|
61
62
|
// concurrency issues during the back-to-back search runs.
|
|
62
63
|
(_a = (_e = process.env).GMAX_WORKER_COUNT) !== null && _a !== void 0 ? _a : (_e.GMAX_WORKER_COUNT = "1");
|
|
@@ -346,8 +347,8 @@ function evaluateOss(response, c, timeMs) {
|
|
|
346
347
|
// expected defined-symbol for the drift-robust match branch.
|
|
347
348
|
chunkMatches(chunk, c.expectedFile, c.expectedLine, c.query));
|
|
348
349
|
const rank = idx + 1; // 0 = miss
|
|
349
|
-
const rr = rank > 0 ? 1 / rank : 0;
|
|
350
350
|
const recall10 = rank > 0 && rank <= 10 ? 1 : 0;
|
|
351
|
+
const rr = recall10 ? 1 / rank : 0;
|
|
351
352
|
return {
|
|
352
353
|
id: c.id,
|
|
353
354
|
query: c.query,
|