grepmax 0.26.48 → 0.26.49
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 +75 -40
- 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 +2 -0
- package/dist/lib/daemon/search-handler.js +3 -0
- package/dist/lib/eval/relevance-baseline.js +172 -0
- package/dist/lib/search/diagnostics.js +95 -0
- package/dist/lib/search/searcher.js +69 -12
- 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.49",
|
|
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,19 @@ 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
|
+
## Experimental Orientation
|
|
209
|
+
|
|
210
|
+
`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.
|
|
211
|
+
|
|
212
|
+
```bash
|
|
213
|
+
gmax surprises --experimental --agent
|
|
214
|
+
gmax surprises --experimental --in packages/app/src --dir-depth 4 --agent
|
|
215
|
+
gmax surprises --experimental --exclude generated --top 10
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
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
219
|
|
|
218
220
|
## Background Daemon
|
|
219
221
|
|
|
@@ -228,7 +230,7 @@ gmax watch restart # Stop it, wait for it to exit, start a fresh one
|
|
|
228
230
|
gmax status # See all projects + watcher status
|
|
229
231
|
```
|
|
230
232
|
|
|
231
|
-
|
|
233
|
+
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
234
|
|
|
233
235
|
`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
236
|
|
|
@@ -277,14 +279,14 @@ On Linux the per-socket list does not exist; the only option is
|
|
|
277
279
|
|
|
278
280
|
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
281
|
|
|
280
|
-
> **Memory footprint.** `gmax llm start`, `investigate`,
|
|
282
|
+
> **Memory footprint.** `gmax llm start`, `investigate`, and `review` can load a multi-GB
|
|
281
283
|
> GGUF model (typical coding models run 16–21 GB). On a memory-constrained machine that can stall
|
|
282
284
|
> or hang the host. Start it deliberately, not as a reflex — and if you run coding agents against
|
|
283
285
|
> this repo, they should be told not to invoke these on their own initiative. The bundled skill
|
|
284
286
|
> already carries that instruction.
|
|
285
287
|
>
|
|
286
|
-
> Everything else in gmax — search, `trace`, `impact`, `context`, `--context-for-llm` —
|
|
287
|
-
> the
|
|
288
|
+
> Everything else in gmax — search, `trace`, `impact`, `context`, `--context-for-llm` — uses
|
|
289
|
+
> local embeddings and the index without a generative LLM.
|
|
288
290
|
|
|
289
291
|
```bash
|
|
290
292
|
gmax llm on # Enable LLM features (persists to config)
|
|
@@ -330,16 +332,23 @@ The hook sends an IPC message to the daemon and returns instantly — it never b
|
|
|
330
332
|
|
|
331
333
|
### LLM Configuration
|
|
332
334
|
|
|
335
|
+
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.
|
|
336
|
+
|
|
333
337
|
| Variable | Description | Default |
|
|
334
338
|
| --- | --- | --- |
|
|
335
|
-
| `GMAX_LLM_MODEL` | Path to GGUF model file |
|
|
339
|
+
| `GMAX_LLM_MODEL` | Path to GGUF model file | Machine-specific fallback |
|
|
340
|
+
| `GMAX_LLM_HOST` | Loopback address (`localhost` maps to IPv4 loopback) | `127.0.0.1` |
|
|
336
341
|
| `GMAX_LLM_BINARY` | llama-server binary | `llama-server` |
|
|
337
342
|
| `GMAX_LLM_PORT` | Server port | `8079` |
|
|
338
343
|
| `GMAX_LLM_IDLE_TIMEOUT` | Minutes before auto-stop | `30` |
|
|
339
344
|
|
|
345
|
+
### Summary compatibility commands
|
|
346
|
+
|
|
347
|
+
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.
|
|
348
|
+
|
|
340
349
|
## Architecture
|
|
341
350
|
|
|
342
|
-
|
|
351
|
+
Default shared data lives in `~/.gmax/`; configured secondary stores retain their own project indexes:
|
|
343
352
|
- `lancedb/` — LanceDB vector store (centralized, all projects)
|
|
344
353
|
- `cache/meta.lmdb` — file metadata cache (hashes, mtimes)
|
|
345
354
|
- `cache/watchers.lmdb` — watcher/daemon registry (LMDB, crash-safe)
|
|
@@ -347,7 +356,9 @@ All data lives in `~/.gmax/`:
|
|
|
347
356
|
- `daemon.pid` — PID file for daemon dedup
|
|
348
357
|
- `logs/` — daemon and server logs (5MB rotation)
|
|
349
358
|
- `config.json` — global config (model tier, embed mode)
|
|
350
|
-
- `models/` — embedding models
|
|
359
|
+
- `models/` — ONNX embedding models
|
|
360
|
+
- `hf/` — pinned Hugging Face cache for MLX embedding models
|
|
361
|
+
- `watch-leases.json` — active session/CLI watch leases
|
|
351
362
|
- `grammars/` — Tree-sitter grammars
|
|
352
363
|
- `projects.json` — registry of indexed directories
|
|
353
364
|
|
|
@@ -378,22 +389,19 @@ gmax embeds with IBM's Granite r2 code-embedding models. Two tiers are available
|
|
|
378
389
|
| `small` (default) | `granite-embedding-small-english-r2` | 384 | 47M |
|
|
379
390
|
| `standard` | `granite-embedding-english-r2` | 768 | 149M |
|
|
380
391
|
|
|
381
|
-
**384d
|
|
382
|
-
|
|
392
|
+
**384d remains the default.** In the recorded 97-case model-tier comparison
|
|
393
|
+
on gmax's own repo, the larger 768d model scored **~10 points
|
|
383
394
|
*worse* on Recall@10** than 384d, consistently on both the MLX GPU and ONNX CPU
|
|
384
395
|
embedding paths, with and without ColBERT rerank:
|
|
385
396
|
|
|
386
|
-
| Model | Recall@10 | MRR
|
|
397
|
+
| Model | Recall@10 | Reported MRR |
|
|
387
398
|
| --- | --- | --- |
|
|
388
399
|
| `small` / 384d | **0.72** | **0.51** |
|
|
389
400
|
| `standard` / 768d | 0.62 | 0.44 |
|
|
390
401
|
|
|
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>
|
|
402
|
+
<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 v0.26.48 benchmark; see the evaluation caveats below.</sub>
|
|
394
403
|
|
|
395
|
-
|
|
396
|
-
worker RAM, and dense-vector storage while *lowering* recall on code search.
|
|
404
|
+
The larger tier performed worse on that fixture and doubles dense-vector width; compute and memory costs depend on the backend.
|
|
397
405
|
Unless you have a measured reason to switch (e.g. a recall complaint on a very
|
|
398
406
|
large repo — benchmark it first), stay on 384d.
|
|
399
407
|
|
|
@@ -438,11 +446,11 @@ fixtures/
|
|
|
438
446
|
| Variable | Description | Default |
|
|
439
447
|
| --- | --- | --- |
|
|
440
448
|
| `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))` |
|
|
449
|
+
| `GMAX_WORKER_THREADS` | Worker processes for embedding; overrides `gmax config --worker-threads` | `min(cores, 4, max(2, floor(cores/2)))` |
|
|
442
450
|
| `GMAX_WORKER_RSS_RECYCLE_MB` | Recycle workers that remain above this RSS; `0` disables the check | `1536` |
|
|
443
451
|
| `GMAX_DEBUG` | Debug logging | Off |
|
|
444
|
-
| `
|
|
445
|
-
| `
|
|
452
|
+
| `GMAX_RERANK` | Force ColBERT rerank (`1`); concentrated candidates can also enable it automatically ([why](docs/known-limitations.md)) | Off |
|
|
453
|
+
| `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
454
|
| `GMAX_ANN` | Experimental IVF_FLAT vector index (`1`); leave off unless validating recall on your corpus | Off |
|
|
447
455
|
|
|
448
456
|
## Troubleshooting
|
|
@@ -466,17 +474,17 @@ Compaction pauses index writes, waits for pending writes to commit, and opens a
|
|
|
466
474
|
|
|
467
475
|
### Known issues
|
|
468
476
|
|
|
469
|
-
|
|
477
|
+
For detection and recovery guidance, check
|
|
470
478
|
[`docs/known-limitations.md`](docs/known-limitations.md) — it covers what each case means and
|
|
471
479
|
whether you need to act.
|
|
472
480
|
|
|
473
481
|
| You see | What it means |
|
|
474
482
|
|---|---|
|
|
475
|
-
| `Optimize panicked ... inverted/builder.rs` |
|
|
483
|
+
| `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
484
|
| `disabling auto-rebuild until an optimize succeeds` | Transient, not a wedge. The next successful optimize clears it. Search stays available. |
|
|
477
485
|
| `ANN: vector index not built` | Normal — exact search is the default. |
|
|
478
486
|
| `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 |
|
|
487
|
+
| `npm audit` reports advisories on install | v0.26.48 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
488
|
|
|
481
489
|
## Contributing
|
|
482
490
|
|
|
@@ -484,7 +492,7 @@ See [CLAUDE.md](CLAUDE.md) for development setup, commands, and architecture det
|
|
|
484
492
|
|
|
485
493
|
### Benchmarks
|
|
486
494
|
|
|
487
|
-
Two evaluation harnesses
|
|
495
|
+
Two retrieval evaluation harnesses emit JSON via `:json` variants. Additional token and experimental-orientation harnesses are available as `bench:tokens` and `bench:surprises`.
|
|
488
496
|
|
|
489
497
|
```bash
|
|
490
498
|
pnpm bench:recall # 97-case internal eval against gmax's own repo
|
|
@@ -494,10 +502,37 @@ pnpm bench:oss:json
|
|
|
494
502
|
GMAX_EVAL_RERANK=1 pnpm bench:oss # toggle ColBERT rerank
|
|
495
503
|
```
|
|
496
504
|
|
|
497
|
-
The OSS
|
|
505
|
+
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.
|
|
506
|
+
|
|
507
|
+
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 was corrected in source after v0.26.48; 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
508
|
|
|
499
509
|
`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
510
|
|
|
511
|
+
#### Frozen multi-repository baseline
|
|
512
|
+
|
|
513
|
+
`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.
|
|
514
|
+
|
|
515
|
+
```bash
|
|
516
|
+
shasum -a 256 docs/measurements/relevance/fixture-v1.json
|
|
517
|
+
pnpm bench:relevance --fixture docs/measurements/relevance/fixture-v1.json \
|
|
518
|
+
--sha256 <reviewed-fixture-digest> \
|
|
519
|
+
--output docs/measurements/relevance/baseline-v1.json --repeats 2
|
|
520
|
+
```
|
|
521
|
+
|
|
522
|
+
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.
|
|
523
|
+
|
|
524
|
+
A hit requires the exact file and either the definition symbol or overlap with the declared line range. 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.
|
|
525
|
+
|
|
526
|
+
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.
|
|
527
|
+
|
|
528
|
+
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.
|
|
529
|
+
|
|
530
|
+
Source now supports `--diagnostics`. This requires a ready daemon advertising `capabilities.searchDiagnostics: 1`; v0.26.48's installed release does not advertise it. 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.
|
|
531
|
+
|
|
532
|
+
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.
|
|
533
|
+
|
|
534
|
+
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.
|
|
535
|
+
|
|
501
536
|
## Attribution
|
|
502
537
|
|
|
503
538
|
grepmax is built upon the foundation of [mgrep](https://github.com/mixedbread-ai/mgrep) by MixedBread. See the [NOTICE](NOTICE) file for details.
|
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,
|
|
@@ -0,0 +1,337 @@
|
|
|
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
|
+
/** Frozen local multi-corpus baseline, using the existing daemon with no fallback. */
|
|
46
|
+
const node_child_process_1 = require("node:child_process");
|
|
47
|
+
const fs = __importStar(require("node:fs"));
|
|
48
|
+
const path = __importStar(require("node:path"));
|
|
49
|
+
const node_util_1 = require("node:util");
|
|
50
|
+
const lmdb_1 = require("lmdb");
|
|
51
|
+
const config_1 = require("./config");
|
|
52
|
+
const relevance_baseline_1 = require("./lib/eval/relevance-baseline");
|
|
53
|
+
const daemon_client_1 = require("./lib/utils/daemon-client");
|
|
54
|
+
const project_registry_1 = require("./lib/utils/project-registry");
|
|
55
|
+
function git(root) {
|
|
56
|
+
try {
|
|
57
|
+
return {
|
|
58
|
+
head: (0, node_child_process_1.execFileSync)("git", ["rev-parse", "HEAD"], {
|
|
59
|
+
cwd: root,
|
|
60
|
+
encoding: "utf8",
|
|
61
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
62
|
+
}).trim(),
|
|
63
|
+
dirty: (0, node_child_process_1.execFileSync)("git", ["status", "--porcelain"], {
|
|
64
|
+
cwd: root,
|
|
65
|
+
encoding: "utf8",
|
|
66
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
67
|
+
}).trim().length > 0,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
catch (_a) {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function run() {
|
|
75
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
76
|
+
var _a, _b, _c, _d;
|
|
77
|
+
var _e, _f;
|
|
78
|
+
const { values } = (0, node_util_1.parseArgs)({
|
|
79
|
+
options: {
|
|
80
|
+
fixture: { type: "string" },
|
|
81
|
+
sha256: { type: "string" },
|
|
82
|
+
output: { type: "string" },
|
|
83
|
+
repeats: { type: "string", default: "2" },
|
|
84
|
+
help: { type: "boolean" },
|
|
85
|
+
diagnostics: { type: "boolean", default: false },
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
if (values.help) {
|
|
89
|
+
console.log("pnpm bench:relevance --fixture <json> --sha256 <frozen-sha256> --output <new-json> [--repeats 2] [--diagnostics]");
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (!values.fixture || !values.sha256 || !values.output)
|
|
93
|
+
throw new Error("Required: --fixture, --sha256, --output");
|
|
94
|
+
if (fs.existsSync(values.output))
|
|
95
|
+
throw new Error("Output exists; use a new artifact path");
|
|
96
|
+
const checkpoint = `${values.output}.samples.jsonl`;
|
|
97
|
+
if (fs.existsSync(checkpoint))
|
|
98
|
+
throw new Error("Checkpoint exists; use a new artifact path");
|
|
99
|
+
const repeats = Number(values.repeats);
|
|
100
|
+
if (!Number.isInteger(repeats) || repeats < 1 || repeats > 5)
|
|
101
|
+
throw new Error("Repeats must be 1–5");
|
|
102
|
+
const fixturePath = path.resolve(values.fixture);
|
|
103
|
+
const fixture = (0, relevance_baseline_1.parseFrozenFixture)(fs.readFileSync(fixturePath), values.sha256);
|
|
104
|
+
const roots = new Map(fixture.corpora.map((r) => [
|
|
105
|
+
r.id,
|
|
106
|
+
fs.realpathSync(path.resolve(path.dirname(fixturePath), r.root)),
|
|
107
|
+
]));
|
|
108
|
+
const startedAt = new Date().toISOString();
|
|
109
|
+
const ping = yield (0, daemon_client_1.sendDaemonCommand)({ cmd: "ping" }, { timeoutMs: 5000 });
|
|
110
|
+
if (!ping.ok || ping.ready !== true)
|
|
111
|
+
throw new Error("An already-ready daemon is required; no autostart/fallback");
|
|
112
|
+
if (values.diagnostics &&
|
|
113
|
+
((_a = ping.capabilities) === null || _a === void 0 ? void 0 : _a.searchDiagnostics) !== 1)
|
|
114
|
+
throw new Error("Daemon lacks searchDiagnostics v1; install/restart a compatible daemon before measuring diagnostics");
|
|
115
|
+
const before = yield (0, daemon_client_1.sendDaemonCommand)({ cmd: "status" });
|
|
116
|
+
if (!before.ok)
|
|
117
|
+
throw new Error("Cannot capture daemon status");
|
|
118
|
+
const registry = (0, project_registry_1.listProjects)();
|
|
119
|
+
const cache = (0, lmdb_1.open)({
|
|
120
|
+
path: path.join(config_1.PATHS.globalRoot, "cache/meta.lmdb"),
|
|
121
|
+
compression: true,
|
|
122
|
+
readOnly: true,
|
|
123
|
+
});
|
|
124
|
+
const samples = [];
|
|
125
|
+
const preflight = [];
|
|
126
|
+
fs.writeFileSync(checkpoint, "", { flag: "wx" });
|
|
127
|
+
try {
|
|
128
|
+
for (const corpus of fixture.corpora) {
|
|
129
|
+
const root = roots.get(corpus.id);
|
|
130
|
+
const project = registry.find((p) => p.root === root);
|
|
131
|
+
if ((project === null || project === void 0 ? void 0 : project.status) !== "indexed")
|
|
132
|
+
throw new Error(`Corpus is not indexed: ${corpus.id}`);
|
|
133
|
+
const files = [
|
|
134
|
+
...new Set(fixture.cases
|
|
135
|
+
.filter((c) => c.corpus === corpus.id)
|
|
136
|
+
.flatMap((c) => c.expected.map((t) => t.file))),
|
|
137
|
+
];
|
|
138
|
+
const rows = yield (0, daemon_client_1.sendDaemonCommand)({
|
|
139
|
+
cmd: "rows.locate",
|
|
140
|
+
projectRoot: root,
|
|
141
|
+
limit: 500,
|
|
142
|
+
select: ["path", "defined_symbols", "start_line", "end_line"],
|
|
143
|
+
matches: files.map((file) => ({ kind: "path", path: file })),
|
|
144
|
+
});
|
|
145
|
+
if (!rows.ok || !Array.isArray(rows.rows))
|
|
146
|
+
throw new Error(`Target inventory failed: ${corpus.id}`);
|
|
147
|
+
preflight.push({
|
|
148
|
+
corpus: corpus.id,
|
|
149
|
+
root,
|
|
150
|
+
git: git(root),
|
|
151
|
+
project,
|
|
152
|
+
files,
|
|
153
|
+
targetRows: rows.rows,
|
|
154
|
+
cache: files.map((file) => (Object.assign({ file }, cache.get(path.join(root, file))))),
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
// Sequential execution bounds load. Interleave corpora instead of running
|
|
158
|
+
// all of one corpus first; repetitions are reported separately from cases.
|
|
159
|
+
const ordered = [];
|
|
160
|
+
const groups = fixture.corpora.map((r) => fixture.cases.filter((c) => c.corpus === r.id));
|
|
161
|
+
for (let i = 0; i < Math.max(...groups.map((g) => g.length)); i++) {
|
|
162
|
+
for (const group of groups)
|
|
163
|
+
if (group[i])
|
|
164
|
+
ordered.push(group[i]);
|
|
165
|
+
}
|
|
166
|
+
for (let repetition = 1; repetition <= repeats; repetition++) {
|
|
167
|
+
for (const c of ordered) {
|
|
168
|
+
const root = roots.get(c.corpus);
|
|
169
|
+
const exclusions = new Set();
|
|
170
|
+
const inventory = preflight.find((r) => r.corpus === c.corpus);
|
|
171
|
+
for (const t of c.expected) {
|
|
172
|
+
const file = path.join(root, t.file);
|
|
173
|
+
if (fs.realpathSync(file) !== file)
|
|
174
|
+
throw new Error(`Target aliases are not frozen: ${c.id}`);
|
|
175
|
+
for (const reason of (0, relevance_baseline_1.sourceExclusions)(t.sourceSha256, (0, relevance_baseline_1.sha256)(fs.readFileSync(file)), (_b = cache.get(file)) === null || _b === void 0 ? void 0 : _b.hash))
|
|
176
|
+
exclusions.add(reason);
|
|
177
|
+
const indexed = inventory.targetRows
|
|
178
|
+
.flat()
|
|
179
|
+
.some((row) => (0, relevance_baseline_1.matchesTarget)({
|
|
180
|
+
type: "text",
|
|
181
|
+
score: 0,
|
|
182
|
+
metadata: { path: String(row.path), hash: "" },
|
|
183
|
+
defined_symbols: row.defined_symbols,
|
|
184
|
+
generated_metadata: {
|
|
185
|
+
start_line: Number(row.start_line),
|
|
186
|
+
num_lines: Number(row.end_line) - Number(row.start_line) + 1,
|
|
187
|
+
},
|
|
188
|
+
}, root, t));
|
|
189
|
+
if (!indexed)
|
|
190
|
+
exclusions.add("target_not_in_index");
|
|
191
|
+
}
|
|
192
|
+
const start = performance.now();
|
|
193
|
+
const response = exclusions.size === 0
|
|
194
|
+
? yield (0, daemon_client_1.sendDaemonCommand)({
|
|
195
|
+
cmd: "search",
|
|
196
|
+
projectRoot: root,
|
|
197
|
+
query: c.query,
|
|
198
|
+
limit: 20,
|
|
199
|
+
pathPrefix: `${root}${path.sep}`,
|
|
200
|
+
rerank: false,
|
|
201
|
+
explain: true,
|
|
202
|
+
diagnostics: values.diagnostics,
|
|
203
|
+
}, { timeoutMs: 60000 })
|
|
204
|
+
: { ok: false, error: "preflight_excluded" };
|
|
205
|
+
const elapsedMs = performance.now() - start;
|
|
206
|
+
if (values.diagnostics &&
|
|
207
|
+
response.ok &&
|
|
208
|
+
((_c = response.diagnostics) === null || _c === void 0 ? void 0 : _c.schemaVersion) !== 1)
|
|
209
|
+
exclusions.add("diagnostics_missing");
|
|
210
|
+
if (!response.ok)
|
|
211
|
+
exclusions.add(String((_e = response.error) !== null && _e !== void 0 ? _e : "search_failed"));
|
|
212
|
+
const data = Array.isArray(response.data)
|
|
213
|
+
? response.data
|
|
214
|
+
: [];
|
|
215
|
+
const state = response.indexState;
|
|
216
|
+
if (state &&
|
|
217
|
+
(state.indexing ||
|
|
218
|
+
state.verifying ||
|
|
219
|
+
state.degraded ||
|
|
220
|
+
Number(state.pendingFiles) > 0 ||
|
|
221
|
+
Number(state.failedFiles) > 0))
|
|
222
|
+
exclusions.add("index_unsettled");
|
|
223
|
+
const warnings = Array.isArray(response.warnings)
|
|
224
|
+
? response.warnings
|
|
225
|
+
: [];
|
|
226
|
+
if (warnings.length)
|
|
227
|
+
exclusions.add("search_warning");
|
|
228
|
+
for (const t of c.expected) {
|
|
229
|
+
const file = path.join(root, t.file);
|
|
230
|
+
for (const reason of (0, relevance_baseline_1.sourceExclusions)(t.sourceSha256, (0, relevance_baseline_1.sha256)(fs.readFileSync(file)), (_d = cache.get(file)) === null || _d === void 0 ? void 0 : _d.hash))
|
|
231
|
+
exclusions.add(reason);
|
|
232
|
+
if (data.some((d) => { var _a; return ((_a = d.metadata) === null || _a === void 0 ? void 0 : _a.path) === file && d.metadata.hash !== t.sourceSha256; }))
|
|
233
|
+
exclusions.add("returned_row_hash_mismatch");
|
|
234
|
+
}
|
|
235
|
+
const ranked = { data };
|
|
236
|
+
samples.push(Object.assign(Object.assign({}, (0, relevance_baseline_1.scoreRelevance)(ranked, root, c)), { id: c.id, corpus: c.corpus, split: c.split, repetition,
|
|
237
|
+
elapsedMs, exclusions: [...exclusions], query: c.query, intent: c.intent, origin: c.origin, warnings, indexState: state !== null && state !== void 0 ? state : null, error: response.ok ? null : response.error, diagnostics: (_f = response.diagnostics) !== null && _f !== void 0 ? _f : null, results: data.map((d) => { var _a, _b; var _c, _d, _e, _f, _g; return ({
|
|
238
|
+
path: path.relative(root, String((_c = (_a = d.metadata) === null || _a === void 0 ? void 0 : _a.path) !== null && _c !== void 0 ? _c : "")),
|
|
239
|
+
hash: (_d = (_b = d.metadata) === null || _b === void 0 ? void 0 : _b.hash) !== null && _d !== void 0 ? _d : null,
|
|
240
|
+
symbols: (_e = d.defined_symbols) !== null && _e !== void 0 ? _e : [],
|
|
241
|
+
range: (_f = d.generated_metadata) !== null && _f !== void 0 ? _f : null,
|
|
242
|
+
score: d.score,
|
|
243
|
+
scoreBreakdown: (_g = d.scoreBreakdown) !== null && _g !== void 0 ? _g : null,
|
|
244
|
+
}); }) }));
|
|
245
|
+
fs.appendFileSync(checkpoint, `${JSON.stringify(samples[samples.length - 1])}\n`);
|
|
246
|
+
console.error(`${c.id} repeat=${repetition} rank=${samples[samples.length - 1].rank} excluded=${[...exclusions].join(",") || "none"}`);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
finally {
|
|
251
|
+
cache.close();
|
|
252
|
+
}
|
|
253
|
+
const afterPing = yield (0, daemon_client_1.sendDaemonCommand)({ cmd: "ping" });
|
|
254
|
+
const after = yield (0, daemon_client_1.sendDaemonCommand)({ cmd: "status" });
|
|
255
|
+
if (!afterPing.ok ||
|
|
256
|
+
after.pid !== before.pid ||
|
|
257
|
+
afterPing.version !== ping.version) {
|
|
258
|
+
for (const sample of samples)
|
|
259
|
+
sample.exclusions.push("daemon_changed");
|
|
260
|
+
}
|
|
261
|
+
if ((0, relevance_baseline_1.sha256)(fs.readFileSync(fixturePath)) !== values.sha256)
|
|
262
|
+
throw new Error("Frozen fixture changed during execution");
|
|
263
|
+
const report = {
|
|
264
|
+
schemaVersion: 1,
|
|
265
|
+
startedAt,
|
|
266
|
+
finishedAt: new Date().toISOString(),
|
|
267
|
+
fixtureSha256: values.sha256,
|
|
268
|
+
fixture,
|
|
269
|
+
source: {
|
|
270
|
+
git: git(process.cwd()),
|
|
271
|
+
node: process.version,
|
|
272
|
+
evaluatorHash: (0, relevance_baseline_1.sha256)(fs.readFileSync(__filename)),
|
|
273
|
+
scoringHash: (0, relevance_baseline_1.sha256)(fs.readFileSync(path.join(__dirname, "lib/eval/relevance-baseline.ts"))),
|
|
274
|
+
searcherHash: (0, relevance_baseline_1.sha256)(fs.readFileSync(path.join(__dirname, "lib/search/searcher.ts"))),
|
|
275
|
+
diagnosticsHash: (0, relevance_baseline_1.sha256)(fs.readFileSync(path.join(__dirname, "lib/search/diagnostics.ts"))),
|
|
276
|
+
},
|
|
277
|
+
execution: {
|
|
278
|
+
transport: "existing-daemon-ipc",
|
|
279
|
+
requestedRerank: false,
|
|
280
|
+
explain: true,
|
|
281
|
+
requestedDiagnostics: values.diagnostics,
|
|
282
|
+
limit: 20,
|
|
283
|
+
repeats,
|
|
284
|
+
ping,
|
|
285
|
+
before,
|
|
286
|
+
afterPing,
|
|
287
|
+
after,
|
|
288
|
+
preflight,
|
|
289
|
+
diagnostics: {
|
|
290
|
+
fusionPool: values.diagnostics
|
|
291
|
+
? "bounded-post-seed-head-200"
|
|
292
|
+
: "unobserved",
|
|
293
|
+
concentrationGate: values.diagnostics
|
|
294
|
+
? "per-sample-diagnostics"
|
|
295
|
+
: "unobserved",
|
|
296
|
+
daemonRankingEnvironment: values.diagnostics
|
|
297
|
+
? "allowlisted-per-sample-diagnostics"
|
|
298
|
+
: "unobserved",
|
|
299
|
+
ftsHealth: values.diagnostics
|
|
300
|
+
? "per-sample-diagnostics"
|
|
301
|
+
: "warnings-and-score-breakdowns-only",
|
|
302
|
+
trueRerankOff: "not-asserted",
|
|
303
|
+
},
|
|
304
|
+
},
|
|
305
|
+
summary: (0, relevance_baseline_1.summarizeRelevance)(samples),
|
|
306
|
+
byCorpus: fixture.corpora.map((r) => (Object.assign({ corpus: r.id }, (0, relevance_baseline_1.summarizeRelevance)(samples.filter((s) => s.corpus === r.id))))),
|
|
307
|
+
bySplit: ["dev", "heldout"].map((split) => (Object.assign({ split }, (0, relevance_baseline_1.summarizeRelevance)(samples.filter((s) => s.split === split))))),
|
|
308
|
+
byIntent: [...new Set(fixture.cases.map((c) => c.intent))].map((intent) => (Object.assign({ intent }, (0, relevance_baseline_1.summarizeRelevance)(samples.filter((s) => s.intent === intent))))),
|
|
309
|
+
samples,
|
|
310
|
+
researchGateReady: false,
|
|
311
|
+
limitations: [
|
|
312
|
+
"Curated source-grounded cases are not observed user-session misses",
|
|
313
|
+
values.diagnostics
|
|
314
|
+
? "Candidate trace is limited to the post-seed fusion head; absence from a truncated trace does not prove retrieval failure"
|
|
315
|
+
: "Pipeline diagnostics were not requested",
|
|
316
|
+
"Client environment cannot change daemon ranking settings",
|
|
317
|
+
"Latencies mix ordinary daemon activity and first/repeated queries",
|
|
318
|
+
],
|
|
319
|
+
};
|
|
320
|
+
fs.writeFileSync(values.output, `${JSON.stringify(report, null, 2)}\n`, {
|
|
321
|
+
flag: "wx",
|
|
322
|
+
});
|
|
323
|
+
console.log(JSON.stringify({
|
|
324
|
+
output: values.output,
|
|
325
|
+
summary: report.summary,
|
|
326
|
+
byCorpus: report.byCorpus,
|
|
327
|
+
bySplit: report.bySplit,
|
|
328
|
+
}, null, 2));
|
|
329
|
+
if (samples.some((s) => s.exclusions.length > 0))
|
|
330
|
+
process.exitCode = 2;
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
if (require.main === module)
|
|
334
|
+
run().catch((error) => {
|
|
335
|
+
console.error(error);
|
|
336
|
+
process.exitCode = 1;
|
|
337
|
+
});
|
package/dist/eval.js
CHANGED
|
@@ -527,9 +527,10 @@ function evaluateCase(response, evalCase, timeMs) {
|
|
|
527
527
|
const avoidRank = response.data.findIndex((chunk) => { var _a, _b, _c; return (_b = (_a = chunk.metadata) === null || _a === void 0 ? void 0 : _a.path) === null || _b === void 0 ? void 0 : _b.toLowerCase().includes(((_c = evalCase.avoidPath) === null || _c === void 0 ? void 0 : _c.toLowerCase()) || "_____"); });
|
|
528
528
|
const hitAvoid = evalCase.avoidPath && avoidRank >= 0 && (rank === -1 || avoidRank < rank);
|
|
529
529
|
const found = rank >= 0 && !hitAvoid;
|
|
530
|
-
const rr = found ? 1 / (rank + 1) : 0;
|
|
531
530
|
const recall = found && rank < 10 ? 1 : 0;
|
|
531
|
+
const rr = recall ? 1 / (rank + 1) : 0;
|
|
532
532
|
return {
|
|
533
|
+
rank: rank + 1,
|
|
533
534
|
rr,
|
|
534
535
|
found,
|
|
535
536
|
recall,
|
|
@@ -619,7 +620,7 @@ function run() {
|
|
|
619
620
|
console.log(`Eval results for store at: ${paths.lancedbDir}`);
|
|
620
621
|
console.log("=".repeat(80));
|
|
621
622
|
results.forEach((r) => {
|
|
622
|
-
const status = r.found ? `rank ${
|
|
623
|
+
const status = r.found ? `rank ${r.rank}` : "❌ missed";
|
|
623
624
|
const emoji = r.found ? (r.rr === 1 ? "🎯" : "✓") : "❌";
|
|
624
625
|
console.log(`${emoji} ${r.query}`);
|
|
625
626
|
console.log(` => ${status} (target: ${r.path}) [${r.timeMs.toFixed(0)}ms]`);
|
|
@@ -628,7 +629,7 @@ function run() {
|
|
|
628
629
|
}
|
|
629
630
|
});
|
|
630
631
|
console.log("=".repeat(80));
|
|
631
|
-
console.log(`MRR: ${mrr.toFixed(3)}`);
|
|
632
|
+
console.log(`MRR@10: ${mrr.toFixed(3)}`);
|
|
632
633
|
console.log(`Recall@10: ${recallAt10.toFixed(3)}`);
|
|
633
634
|
console.log(`Avg query time: ${avgTime.toFixed(0)}ms`);
|
|
634
635
|
console.log(`Total time: ${totalTime.toFixed(0)}ms`);
|
|
@@ -152,6 +152,7 @@ function handleCommand(daemon, cmd, conn) {
|
|
|
152
152
|
capabilities: {
|
|
153
153
|
exclusiveGenerationRebuild: exports.EXCLUSIVE_GENERATION_REBUILD_PROTOCOL,
|
|
154
154
|
readVerbs: read_verbs_1.READ_VERBS_PROTOCOL,
|
|
155
|
+
searchDiagnostics: 1,
|
|
155
156
|
// `watch`/`unwatch` accept holder/pid/ttlMs. A daemon without this
|
|
156
157
|
// treats `unwatch` as unwatch-for-everyone, so clients releasing a
|
|
157
158
|
// single lease must check it first.
|
|
@@ -299,6 +300,7 @@ function handleCommand(daemon, cmd, conn) {
|
|
|
299
300
|
pathPrefix,
|
|
300
301
|
rerank: cmd.rerank === true,
|
|
301
302
|
explain: cmd.explain === true,
|
|
303
|
+
diagnostics: cmd.diagnostics === true,
|
|
302
304
|
seeds: cmd.seeds &&
|
|
303
305
|
typeof cmd.seeds === "object" &&
|
|
304
306
|
!Array.isArray(cmd.seeds)
|
|
@@ -103,6 +103,7 @@ function handleDaemonSearch(deps, payload, signal) {
|
|
|
103
103
|
result = yield searcher.search(payload.query, payload.limit, {
|
|
104
104
|
rerank: payload.rerank === true,
|
|
105
105
|
explain: payload.explain === true,
|
|
106
|
+
diagnostics: payload.diagnostics === true,
|
|
106
107
|
seeds: payload.seeds,
|
|
107
108
|
}, payload.filters, payload.pathPrefix, undefined, signal);
|
|
108
109
|
}
|
|
@@ -116,6 +117,8 @@ function handleDaemonSearch(deps, payload, signal) {
|
|
|
116
117
|
const response = { ok: true, data: result.data };
|
|
117
118
|
if ((_c = result.warnings) === null || _c === void 0 ? void 0 : _c.length)
|
|
118
119
|
response.warnings = result.warnings;
|
|
120
|
+
if (payload.diagnostics === true && result.diagnostics)
|
|
121
|
+
response.diagnostics = result.diagnostics;
|
|
119
122
|
// Annotate partial results when the index is still catching up, so an
|
|
120
123
|
// agent can caveat or retry. Settled failures remain visible too.
|
|
121
124
|
const idx = deps.getIndexState(root);
|
|
@@ -0,0 +1,172 @@
|
|
|
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
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.relevanceFixtureSchema = void 0;
|
|
37
|
+
exports.sha256 = sha256;
|
|
38
|
+
exports.parseFrozenFixture = parseFrozenFixture;
|
|
39
|
+
exports.matchesTarget = matchesTarget;
|
|
40
|
+
exports.scoreRelevance = scoreRelevance;
|
|
41
|
+
exports.summarizeRelevance = summarizeRelevance;
|
|
42
|
+
exports.sourceExclusions = sourceExclusions;
|
|
43
|
+
const node_crypto_1 = require("node:crypto");
|
|
44
|
+
const path = __importStar(require("node:path"));
|
|
45
|
+
const zod_1 = require("zod");
|
|
46
|
+
const digest = zod_1.z.string().regex(/^[a-f0-9]{64}$/);
|
|
47
|
+
const targetSchema = zod_1.z
|
|
48
|
+
.object({
|
|
49
|
+
file: zod_1.z.string().min(1),
|
|
50
|
+
symbol: zod_1.z.string().min(1),
|
|
51
|
+
startLine: zod_1.z.number().int().positive(),
|
|
52
|
+
endLine: zod_1.z.number().int().positive(),
|
|
53
|
+
sourceSha256: digest,
|
|
54
|
+
})
|
|
55
|
+
.strict()
|
|
56
|
+
.refine((t) => t.endLine >= t.startLine, "Reversed target range");
|
|
57
|
+
exports.relevanceFixtureSchema = zod_1.z
|
|
58
|
+
.object({
|
|
59
|
+
schemaVersion: zod_1.z.literal(1),
|
|
60
|
+
createdAt: zod_1.z.string().min(1),
|
|
61
|
+
purpose: zod_1.z.string().min(1),
|
|
62
|
+
corpora: zod_1.z
|
|
63
|
+
.array(zod_1.z
|
|
64
|
+
.object({
|
|
65
|
+
id: zod_1.z.string().min(1),
|
|
66
|
+
root: zod_1.z.string().min(1),
|
|
67
|
+
language: zod_1.z.string().min(1),
|
|
68
|
+
})
|
|
69
|
+
.strict())
|
|
70
|
+
.min(1),
|
|
71
|
+
cases: zod_1.z
|
|
72
|
+
.array(zod_1.z
|
|
73
|
+
.object({
|
|
74
|
+
id: zod_1.z.string().min(1),
|
|
75
|
+
corpus: zod_1.z.string().min(1),
|
|
76
|
+
split: zod_1.z.enum(["dev", "heldout"]),
|
|
77
|
+
origin: zod_1.z.enum(["curated-source", "observed-session"]),
|
|
78
|
+
intent: zod_1.z.string().min(1),
|
|
79
|
+
query: zod_1.z.string().min(1),
|
|
80
|
+
expected: zod_1.z.array(targetSchema).min(1),
|
|
81
|
+
})
|
|
82
|
+
.strict())
|
|
83
|
+
.min(1),
|
|
84
|
+
})
|
|
85
|
+
.strict()
|
|
86
|
+
.superRefine((fixture, ctx) => {
|
|
87
|
+
for (const values of [fixture.corpora, fixture.cases]) {
|
|
88
|
+
if (new Set(values.map((v) => v.id)).size !== values.length) {
|
|
89
|
+
ctx.addIssue({ code: "custom", message: "Duplicate fixture IDs" });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
for (const c of fixture.cases) {
|
|
93
|
+
if (!fixture.corpora.some((r) => r.id === c.corpus)) {
|
|
94
|
+
ctx.addIssue({
|
|
95
|
+
code: "custom",
|
|
96
|
+
message: `Unknown corpus: ${c.corpus}`,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
for (const target of c.expected) {
|
|
100
|
+
if (path.isAbsolute(target.file) ||
|
|
101
|
+
target.file.split(/[\\/]/).includes("..")) {
|
|
102
|
+
ctx.addIssue({
|
|
103
|
+
code: "custom",
|
|
104
|
+
message: "Targets must be contained relative paths",
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
function sha256(bytes) {
|
|
111
|
+
return (0, node_crypto_1.createHash)("sha256").update(bytes).digest("hex");
|
|
112
|
+
}
|
|
113
|
+
function parseFrozenFixture(bytes, expectedSha256) {
|
|
114
|
+
if (!digest.safeParse(expectedSha256).success ||
|
|
115
|
+
sha256(bytes) !== expectedSha256) {
|
|
116
|
+
throw new Error("Fixture checksum mismatch; freeze the reviewed fixture before measuring");
|
|
117
|
+
}
|
|
118
|
+
return exports.relevanceFixtureSchema.parse(JSON.parse(bytes.toString("utf8")));
|
|
119
|
+
}
|
|
120
|
+
/** Exact file plus definition symbol OR overlap with a source-grounded line range. */
|
|
121
|
+
function matchesTarget(chunk, root, target) {
|
|
122
|
+
var _a, _b, _c, _d;
|
|
123
|
+
if (typeof ((_a = chunk.metadata) === null || _a === void 0 ? void 0 : _a.path) !== "string")
|
|
124
|
+
return false;
|
|
125
|
+
if (path.resolve(chunk.metadata.path) !== path.resolve(root, target.file))
|
|
126
|
+
return false;
|
|
127
|
+
if ((_b = chunk.defined_symbols) === null || _b === void 0 ? void 0 : _b.includes(target.symbol))
|
|
128
|
+
return true;
|
|
129
|
+
const start = (_c = chunk.generated_metadata) === null || _c === void 0 ? void 0 : _c.start_line;
|
|
130
|
+
const count = (_d = chunk.generated_metadata) === null || _d === void 0 ? void 0 : _d.num_lines;
|
|
131
|
+
if (typeof start !== "number" || typeof count !== "number" || count <= 0)
|
|
132
|
+
return false;
|
|
133
|
+
// SearchResponse ranges start at zero; source fixtures use one-based lines.
|
|
134
|
+
return start + 1 <= target.endLine && start + count >= target.startLine;
|
|
135
|
+
}
|
|
136
|
+
function scoreRelevance(response, root, c) {
|
|
137
|
+
const index = response.data.findIndex((chunk) => c.expected.some((t) => matchesTarget(chunk, root, t)));
|
|
138
|
+
const rank = index + 1;
|
|
139
|
+
return {
|
|
140
|
+
rank,
|
|
141
|
+
rrAt10: rank > 0 && rank <= 10 ? 1 / rank : 0,
|
|
142
|
+
recallAt10: rank > 0 && rank <= 10 ? 1 : 0,
|
|
143
|
+
hitAt1: rank === 1 ? 1 : 0,
|
|
144
|
+
found: rank > 0,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
function summarizeRelevance(samples) {
|
|
148
|
+
const valid = samples.filter((s) => s.exclusions.length === 0);
|
|
149
|
+
const times = valid.map((s) => s.elapsedMs).sort((a, b) => a - b);
|
|
150
|
+
const mean = (key) => valid.length
|
|
151
|
+
? valid.reduce((sum, s) => sum + s[key], 0) / valid.length
|
|
152
|
+
: null;
|
|
153
|
+
return {
|
|
154
|
+
cases: new Set(samples.map((s) => s.id)).size,
|
|
155
|
+
samples: samples.length,
|
|
156
|
+
validSamples: valid.length,
|
|
157
|
+
excludedSamples: samples.length - valid.length,
|
|
158
|
+
mrrAt10: mean("rrAt10"),
|
|
159
|
+
recallAt10: mean("recallAt10"),
|
|
160
|
+
hitsAt1: mean("hitAt1"),
|
|
161
|
+
medianMs: times.length ? times[Math.ceil(times.length / 2) - 1] : null,
|
|
162
|
+
p95Ms: times.length ? times[Math.ceil(times.length * 0.95) - 1] : null,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
function sourceExclusions(frozenHash, sourceHash, cacheHash) {
|
|
166
|
+
const reasons = [];
|
|
167
|
+
if (sourceHash !== frozenHash)
|
|
168
|
+
reasons.push("source_changed");
|
|
169
|
+
if (cacheHash !== frozenHash)
|
|
170
|
+
reasons.push("index_cache_hash_mismatch");
|
|
171
|
+
return reasons;
|
|
172
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SearchDiagnosticCollector = void 0;
|
|
4
|
+
const stages = [
|
|
5
|
+
"vector",
|
|
6
|
+
"fts",
|
|
7
|
+
"rrf",
|
|
8
|
+
"fusion",
|
|
9
|
+
"stage1",
|
|
10
|
+
"pooled",
|
|
11
|
+
"rerank",
|
|
12
|
+
"scored",
|
|
13
|
+
"dedup",
|
|
14
|
+
"final",
|
|
15
|
+
];
|
|
16
|
+
const rankingKeys = [
|
|
17
|
+
"GMAX_PRE_K",
|
|
18
|
+
"GMAX_STAGE1_K",
|
|
19
|
+
"GMAX_STAGE2_K",
|
|
20
|
+
"GMAX_RERANK_TOP",
|
|
21
|
+
"GMAX_RERANK_BLEND",
|
|
22
|
+
"GMAX_MAX_PER_FILE",
|
|
23
|
+
"GMAX_DEF_BOOST",
|
|
24
|
+
"GMAX_CONCENTRATION_THRESHOLD",
|
|
25
|
+
"GMAX_ANCHOR_PENALTY",
|
|
26
|
+
"GMAX_TEST_PENALTY",
|
|
27
|
+
"GMAX_DOC_PENALTY",
|
|
28
|
+
"GMAX_PAGERANK",
|
|
29
|
+
"GMAX_PR_WEIGHT",
|
|
30
|
+
"GMAX_SEED_FILE_W",
|
|
31
|
+
"GMAX_SEED_SYMBOL_DEF_W",
|
|
32
|
+
"GMAX_SEED_SYMBOL_REF_W",
|
|
33
|
+
"GMAX_SEED_MAX_RANK",
|
|
34
|
+
"GMAX_ANN",
|
|
35
|
+
"GMAX_ANN_NPROBES",
|
|
36
|
+
"GMAX_ANN_MAX_NPROBES",
|
|
37
|
+
];
|
|
38
|
+
const key = (r) => r.id || `${r.path}:${r.chunk_index}`;
|
|
39
|
+
/** Request-local pointer trace. No content, vectors or additional database reads. */
|
|
40
|
+
class SearchDiagnosticCollector {
|
|
41
|
+
constructor() {
|
|
42
|
+
this.ranks = new Map();
|
|
43
|
+
this.cohort = [];
|
|
44
|
+
this.total = 0;
|
|
45
|
+
this.fileLimited = new Set();
|
|
46
|
+
}
|
|
47
|
+
noteFileLimit(record) {
|
|
48
|
+
this.fileLimited.add(key(record));
|
|
49
|
+
}
|
|
50
|
+
outcome(record) {
|
|
51
|
+
var _a, _b, _c, _d, _e, _f;
|
|
52
|
+
const id = key(record);
|
|
53
|
+
if ((_a = this.ranks.get("final")) === null || _a === void 0 ? void 0 : _a.has(id))
|
|
54
|
+
return "returned";
|
|
55
|
+
if (!((_b = this.ranks.get("stage1")) === null || _b === void 0 ? void 0 : _b.has(id)))
|
|
56
|
+
return "stage1-cut";
|
|
57
|
+
if (!((_c = this.ranks.get("pooled")) === null || _c === void 0 ? void 0 : _c.has(id)) &&
|
|
58
|
+
!((_d = this.ranks.get("scored")) === null || _d === void 0 ? void 0 : _d.has(id)))
|
|
59
|
+
return "pooled-cut";
|
|
60
|
+
if (!((_e = this.ranks.get("scored")) === null || _e === void 0 ? void 0 : _e.has(id)))
|
|
61
|
+
return "not-scored";
|
|
62
|
+
if (!((_f = this.ranks.get("dedup")) === null || _f === void 0 ? void 0 : _f.has(id)))
|
|
63
|
+
return "deduplicated";
|
|
64
|
+
if (this.fileLimited.has(id))
|
|
65
|
+
return "per-file-limit";
|
|
66
|
+
// After the display window fills, remaining candidates are unexamined.
|
|
67
|
+
return "display-limit";
|
|
68
|
+
}
|
|
69
|
+
capture(stage, records) {
|
|
70
|
+
this.ranks.set(stage, new Map(records.map((r, i) => [key(r), i + 1])));
|
|
71
|
+
if (stage === "fusion") {
|
|
72
|
+
this.total = records.length;
|
|
73
|
+
this.cohort = records.slice(0, 200);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
finish(details) {
|
|
77
|
+
return Object.assign(Object.assign({ schemaVersion: 1 }, details), { rankingEnvironment: Object.fromEntries(rankingKeys.map((name) => { var _a; return [name, (_a = process.env[name]) !== null && _a !== void 0 ? _a : null]; })), stages: Object.fromEntries(stages.map((s) => { var _a; var _b; return [s, (_b = (_a = this.ranks.get(s)) === null || _a === void 0 ? void 0 : _a.size) !== null && _b !== void 0 ? _b : 0]; })), trace: {
|
|
78
|
+
cohort: "post-seed-fusion-head",
|
|
79
|
+
limit: 200,
|
|
80
|
+
total: this.total,
|
|
81
|
+
truncated: this.total > this.cohort.length,
|
|
82
|
+
candidates: this.cohort.map((r) => { var _a; return ({
|
|
83
|
+
id: key(r),
|
|
84
|
+
path: r.path,
|
|
85
|
+
hash: r.hash,
|
|
86
|
+
startLine: r.start_line + 1,
|
|
87
|
+
endLine: r.end_line + 1,
|
|
88
|
+
parentSymbol: (_a = r.parent_symbol) !== null && _a !== void 0 ? _a : "",
|
|
89
|
+
outcome: this.outcome(r),
|
|
90
|
+
ranks: Object.fromEntries(stages.map((s) => { var _a; var _b; return [s, (_b = (_a = this.ranks.get(s)) === null || _a === void 0 ? void 0 : _a.get(key(r))) !== null && _b !== void 0 ? _b : 0]; })),
|
|
91
|
+
}); }),
|
|
92
|
+
} });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
exports.SearchDiagnosticCollector = SearchDiagnosticCollector;
|
|
@@ -18,6 +18,7 @@ const filter_builder_1 = require("../utils/filter-builder");
|
|
|
18
18
|
const logger_1 = require("../utils/logger");
|
|
19
19
|
const query_timeout_1 = require("../utils/query-timeout");
|
|
20
20
|
const pool_1 = require("../workers/pool");
|
|
21
|
+
const diagnostics_1 = require("./diagnostics");
|
|
21
22
|
const intent_1 = require("./intent");
|
|
22
23
|
const pagerank_1 = require("./pagerank");
|
|
23
24
|
const seed_weight_1 = require("./seed-weight");
|
|
@@ -403,6 +404,14 @@ class Searcher {
|
|
|
403
404
|
// fused scores ~30:1 so blend tuning can't recover the loss.
|
|
404
405
|
let doRerank = (_a = _search_options === null || _search_options === void 0 ? void 0 : _search_options.rerank) !== null && _a !== void 0 ? _a : false;
|
|
405
406
|
const explain = (_b = _search_options === null || _search_options === void 0 ? void 0 : _search_options.explain) !== null && _b !== void 0 ? _b : false;
|
|
407
|
+
const diagnostics = (_search_options === null || _search_options === void 0 ? void 0 : _search_options.diagnostics)
|
|
408
|
+
? new diagnostics_1.SearchDiagnosticCollector()
|
|
409
|
+
: undefined;
|
|
410
|
+
const requestedRerank = doRerank;
|
|
411
|
+
let gateEvaluated = false;
|
|
412
|
+
let gateShare = null;
|
|
413
|
+
let gateActivated = false;
|
|
414
|
+
let rerankInvoked = false;
|
|
406
415
|
// Aider-style seeding (Phase 4): bias candidate generation toward the
|
|
407
416
|
// agent's working context. Inert unless the caller supplied seed files or
|
|
408
417
|
// symbols, so the default search path is unchanged.
|
|
@@ -569,6 +578,9 @@ class Searcher {
|
|
|
569
578
|
.sort((a, b) => b[1] - a[1])
|
|
570
579
|
.map(([key]) => docMap.get(key))
|
|
571
580
|
.filter(Boolean);
|
|
581
|
+
diagnostics === null || diagnostics === void 0 ? void 0 : diagnostics.capture("vector", vectorResults);
|
|
582
|
+
diagnostics === null || diagnostics === void 0 ? void 0 : diagnostics.capture("fts", ftsResults);
|
|
583
|
+
diagnostics === null || diagnostics === void 0 ? void 0 : diagnostics.capture("rrf", fused);
|
|
572
584
|
// Free raw search results — docMap holds the only needed references
|
|
573
585
|
vectorResults.length = 0;
|
|
574
586
|
ftsResults.length = 0;
|
|
@@ -618,16 +630,16 @@ class Searcher {
|
|
|
618
630
|
// pools (express, platform) it regresses. So detect the concentrated regime
|
|
619
631
|
// here and *add* rerank-on for it. This only ever flips doRerank false→true:
|
|
620
632
|
// an explicit GMAX_RERANK=1 (doRerank already true) is never overridden off.
|
|
633
|
+
const envConcThreshold = Number.parseFloat((_f = process.env.GMAX_CONCENTRATION_THRESHOLD) !== null && _f !== void 0 ? _f : "");
|
|
634
|
+
// <= 0 (or NaN with the default) keeps the gate active at 0.7; a value > 1
|
|
635
|
+
// disables it (no possible share reaches it), giving a rerank-fully-off
|
|
636
|
+
// baseline for sweeps without touching the doRerank default. 0.7 is the
|
|
637
|
+
// sweep winner: highest threshold (least spurious firing) that still
|
|
638
|
+
// retains lodash's +0.15 MRR lift while leaving express/platform flat.
|
|
639
|
+
const CONCENTRATION_THRESHOLD = Number.isFinite(envConcThreshold) && envConcThreshold > 0
|
|
640
|
+
? envConcThreshold
|
|
641
|
+
: 0.7;
|
|
621
642
|
if (!doRerank) {
|
|
622
|
-
const envConcThreshold = Number.parseFloat((_f = process.env.GMAX_CONCENTRATION_THRESHOLD) !== null && _f !== void 0 ? _f : "");
|
|
623
|
-
// <= 0 (or NaN with the default) keeps the gate active at 0.7; a value > 1
|
|
624
|
-
// disables it (no possible share reaches it), giving a rerank-fully-off
|
|
625
|
-
// baseline for sweeps without touching the doRerank default. 0.7 is the
|
|
626
|
-
// sweep winner: highest threshold (least spurious firing) that still
|
|
627
|
-
// retains lodash's +0.15 MRR lift while leaving express/platform flat.
|
|
628
|
-
const CONCENTRATION_THRESHOLD = Number.isFinite(envConcThreshold) && envConcThreshold > 0
|
|
629
|
-
? envConcThreshold
|
|
630
|
-
: 0.7;
|
|
631
643
|
// Histogram a fixed top-K window (not finalLimit) so the threshold stays
|
|
632
644
|
// calibrated across callers regardless of the result window they request.
|
|
633
645
|
const CONCENTRATION_K = 10;
|
|
@@ -643,16 +655,21 @@ class Searcher {
|
|
|
643
655
|
maxBucket = count;
|
|
644
656
|
}
|
|
645
657
|
const share = maxBucket / window.length;
|
|
658
|
+
gateEvaluated = true;
|
|
659
|
+
gateShare = share;
|
|
646
660
|
if (share >= CONCENTRATION_THRESHOLD) {
|
|
647
661
|
doRerank = true;
|
|
662
|
+
gateActivated = true;
|
|
648
663
|
}
|
|
649
664
|
}
|
|
650
665
|
}
|
|
666
|
+
diagnostics === null || diagnostics === void 0 ? void 0 : diagnostics.capture("fusion", fused);
|
|
651
667
|
// Item 8: Widen PRE_RERANK_K
|
|
652
668
|
// Retrieve a wide set for Stage 1 filtering
|
|
653
669
|
const envStage1 = Number.parseInt((_h = process.env.GMAX_STAGE1_K) !== null && _h !== void 0 ? _h : "", 10);
|
|
654
670
|
const STAGE1_K = Number.isFinite(envStage1) && envStage1 > 0 ? envStage1 : 200;
|
|
655
671
|
const topCandidates = fused.slice(0, STAGE1_K);
|
|
672
|
+
diagnostics === null || diagnostics === void 0 ? void 0 : diagnostics.capture("stage1", topCandidates);
|
|
656
673
|
// Free docMap — topCandidates already holds record references
|
|
657
674
|
docMap.clear();
|
|
658
675
|
// Item 9: Two-stage rerank
|
|
@@ -694,8 +711,35 @@ class Searcher {
|
|
|
694
711
|
withScore.sort((a, b) => b.score - a.score);
|
|
695
712
|
stage2Candidates = withScore.slice(0, STAGE2_K).map((x) => x.doc);
|
|
696
713
|
}
|
|
714
|
+
diagnostics === null || diagnostics === void 0 ? void 0 : diagnostics.capture("pooled", stage2Candidates);
|
|
715
|
+
const diagnosticDetails = () => ({
|
|
716
|
+
settings: {
|
|
717
|
+
finalLimit,
|
|
718
|
+
preK: PRE_RERANK_K,
|
|
719
|
+
rrfK: RRF_K,
|
|
720
|
+
stage1K: STAGE1_K,
|
|
721
|
+
stage2K: STAGE2_K,
|
|
722
|
+
rerankTop: RERANK_TOP,
|
|
723
|
+
fusedWeight: FUSED_WEIGHT,
|
|
724
|
+
pagerankEnabled,
|
|
725
|
+
seedsActive: seedCtx.active,
|
|
726
|
+
symbolQuery,
|
|
727
|
+
pooledFilterApplied: !!queryPooled && topCandidates.length > STAGE2_K,
|
|
728
|
+
},
|
|
729
|
+
gate: {
|
|
730
|
+
requestedRerank,
|
|
731
|
+
threshold: CONCENTRATION_THRESHOLD,
|
|
732
|
+
evaluated: gateEvaluated,
|
|
733
|
+
share: gateShare,
|
|
734
|
+
activated: gateActivated,
|
|
735
|
+
rerankInvoked,
|
|
736
|
+
},
|
|
737
|
+
fts: { available: this.ftsAvailable, searchFailed: ftsSearchFailed },
|
|
738
|
+
});
|
|
697
739
|
if (stage2Candidates.length === 0) {
|
|
698
|
-
return { data: [] }
|
|
740
|
+
return Object.assign({ data: [] }, (diagnostics
|
|
741
|
+
? { diagnostics: diagnostics.finish(diagnosticDetails()) }
|
|
742
|
+
: {}));
|
|
699
743
|
}
|
|
700
744
|
const envMaxPerFile = Number.parseInt((_m = process.env.GMAX_MAX_PER_FILE) !== null && _m !== void 0 ? _m : "", 10);
|
|
701
745
|
const MAX_PER_FILE = Number.isFinite(envMaxPerFile) && envMaxPerFile > 0 ? envMaxPerFile : 3;
|
|
@@ -735,6 +779,8 @@ class Searcher {
|
|
|
735
779
|
}
|
|
736
780
|
}
|
|
737
781
|
}
|
|
782
|
+
// Selected rerank membership is separate from actual batch execution.
|
|
783
|
+
diagnostics === null || diagnostics === void 0 ? void 0 : diagnostics.capture("rerank", rerankCandidates);
|
|
738
784
|
// Phase B: Lazy-load colbert data only for the ~20 rerank candidates
|
|
739
785
|
if (doRerank && rerankCandidates.length > 0) {
|
|
740
786
|
const rerankIds = rerankCandidates
|
|
@@ -761,6 +807,7 @@ class Searcher {
|
|
|
761
807
|
}
|
|
762
808
|
const rerankScoreByKey = new Map();
|
|
763
809
|
if (doRerank && rerankCandidates.length > 0) {
|
|
810
|
+
rerankInvoked = true;
|
|
764
811
|
const scores = yield pool.rerank({
|
|
765
812
|
query: queryMatrixRaw,
|
|
766
813
|
docs: rerankCandidates.map((doc) => { var _a; return ({
|
|
@@ -847,8 +894,10 @@ class Searcher {
|
|
|
847
894
|
}
|
|
848
895
|
// Note: "boosted" was not previously declared -- fix to use "scored"
|
|
849
896
|
scored.sort((a, b) => b.score - a.score);
|
|
897
|
+
diagnostics === null || diagnostics === void 0 ? void 0 : diagnostics.capture("scored", scored.map((item) => item.record));
|
|
850
898
|
// Item 11: Intelligent Deduplication
|
|
851
899
|
const uniqueScored = this.deduplicateResults(scored);
|
|
900
|
+
diagnostics === null || diagnostics === void 0 ? void 0 : diagnostics.capture("dedup", uniqueScored.map((item) => item.record));
|
|
852
901
|
// Item 10: Per-file diversification
|
|
853
902
|
const seenFiles = new Map();
|
|
854
903
|
const diversified = [];
|
|
@@ -859,9 +908,13 @@ class Searcher {
|
|
|
859
908
|
diversified.push(item);
|
|
860
909
|
seenFiles.set(path, count + 1);
|
|
861
910
|
}
|
|
911
|
+
else {
|
|
912
|
+
diagnostics === null || diagnostics === void 0 ? void 0 : diagnostics.noteFileLimit(item.record);
|
|
913
|
+
}
|
|
862
914
|
if (diversified.length >= finalLimit)
|
|
863
915
|
break;
|
|
864
916
|
}
|
|
917
|
+
diagnostics === null || diagnostics === void 0 ? void 0 : diagnostics.capture("final", diversified.map((item) => item.record));
|
|
865
918
|
// Phase C: Lazy-load display columns only for the final ~10 results
|
|
866
919
|
const finalIds = diversified
|
|
867
920
|
.map((item) => item.record.id)
|
|
@@ -898,7 +951,7 @@ class Searcher {
|
|
|
898
951
|
const finalResults = diversified.map((item) => (Object.assign(Object.assign({}, item.record), { _score: item.score, _breakdown: item.breakdown, vector: undefined, colbert: undefined })));
|
|
899
952
|
// Item 12: Score Calibration
|
|
900
953
|
const maxScore = finalResults.length > 0 ? finalResults[0]._score : 1.0;
|
|
901
|
-
return Object.assign({ data: finalResults.map((r) => {
|
|
954
|
+
return Object.assign(Object.assign({ data: finalResults.map((r) => {
|
|
902
955
|
const chunk = this.mapRecordToChunk(r, r._score || 0);
|
|
903
956
|
// Normalize score relative to top result
|
|
904
957
|
const normalized = maxScore > 0 ? r._score / maxScore : 0;
|
|
@@ -913,7 +966,11 @@ class Searcher {
|
|
|
913
966
|
}
|
|
914
967
|
chunk.confidence = confidence;
|
|
915
968
|
return chunk;
|
|
916
|
-
}) }, (
|
|
969
|
+
}) }, (diagnostics
|
|
970
|
+
? {
|
|
971
|
+
diagnostics: diagnostics.finish(Object.assign(Object.assign({}, diagnosticDetails()), { settings: Object.assign(Object.assign({}, diagnosticDetails().settings), { maxPerFile: MAX_PER_FILE, definitionBoost: DEF_MATCH_BOOST }) })),
|
|
972
|
+
}
|
|
973
|
+
: {})), (!this.ftsAvailable || ftsSearchFailed
|
|
917
974
|
? {
|
|
918
975
|
warnings: [
|
|
919
976
|
"Full-text search unavailable — results may be less precise",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "grepmax",
|
|
3
|
-
"version": "0.26.
|
|
3
|
+
"version": "0.26.49",
|
|
4
4
|
"author": "Robert Owens <78518764+reowens@users.noreply.github.com>",
|
|
5
5
|
"homepage": "https://github.com/reowens/grepmax",
|
|
6
6
|
"bugs": {
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
"bench:recall:json": "GMAX_EVAL_JSON=1 npx tsx src/eval.ts",
|
|
30
30
|
"bench:oss": "npx tsx src/eval-oss.ts all",
|
|
31
31
|
"bench:oss:json": "GMAX_EVAL_JSON=1 npx tsx src/eval-oss.ts all",
|
|
32
|
+
"bench:relevance": "tsx src/eval-relevance.ts",
|
|
32
33
|
"bench:tokens": "npx tsx src/eval-tokens.ts",
|
|
33
34
|
"bench:tokens:json": "GMAX_EVAL_JSON=1 npx tsx src/eval-tokens.ts",
|
|
34
35
|
"bench:surprises": "npx tsx src/eval-surprising-connections.ts",
|
|
@@ -76,7 +77,7 @@
|
|
|
76
77
|
"NOTICE"
|
|
77
78
|
],
|
|
78
79
|
"license": "Apache-2.0",
|
|
79
|
-
"description": "Semantic code search for coding agents. Local embeddings,
|
|
80
|
+
"description": "Semantic code search for coding agents. Local embeddings, call graph tracing, compact code context.",
|
|
80
81
|
"dependencies": {
|
|
81
82
|
"@clack/prompts": "^1.8.1",
|
|
82
83
|
"@huggingface/transformers": "^4.3.0",
|