grepmax 0.26.49 → 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.
@@ -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.49",
13
+ "version": "0.26.50",
14
14
  "author": {
15
15
  "name": "Robert Owens",
16
16
  "email": "robowens@me.com"
package/README.md CHANGED
@@ -205,6 +205,8 @@ gmax "query" [options]
205
205
  | `--exclude-projects <list>` | With `--all-projects`, skip these projects. | — |
206
206
  | `--min-score <n>` | Minimum score relative to this query’s top match, not an absolute confidence threshold. | `0` |
207
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
+
208
210
  ## Experimental Orientation
209
211
 
210
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.
@@ -399,7 +401,7 @@ embedding paths, with and without ColBERT rerank:
399
401
  | `small` / 384d | **0.72** | **0.51** |
400
402
  | `standard` / 768d | 0.62 | 0.44 |
401
403
 
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>
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>
403
405
 
404
406
  The larger tier performed worse on that fixture and doubles dense-vector width; compute and memory costs depend on the backend.
405
407
  Unless you have a measured reason to switch (e.g. a recall complaint on a very
@@ -484,7 +486,7 @@ whether you need to act.
484
486
  | `disabling auto-rebuild until an optimize succeeds` | Transient, not a wedge. The next successful optimize clears it. Search stays available. |
485
487
  | `ANN: vector index not built` | Normal — exact search is the default. |
486
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. |
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. |
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. |
488
490
 
489
491
  ## Contributing
490
492
 
@@ -504,7 +506,7 @@ GMAX_EVAL_RERANK=1 pnpm bench:oss # toggle ColBERT rerank
504
506
 
505
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.
506
508
 
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.
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.
508
510
 
509
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.
510
512
 
@@ -521,13 +523,13 @@ pnpm bench:relevance --fixture docs/measurements/relevance/fixture-v1.json \
521
523
 
522
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.
523
525
 
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.
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.
525
527
 
526
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.
527
529
 
528
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.
529
531
 
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.
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.
531
533
 
532
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.
533
535
 
@@ -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 (_h) {
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 (_j) { }
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 = (_e = (_d = r.path) !== null && _d !== void 0 ? _d : (_b = r.metadata) === null || _b === void 0 ? void 0 : _b.path) !== null && _e !== void 0 ? _e : "";
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 = (_g = (_f = r.path) !== null && _f !== void 0 ? _f : (_c = r.metadata) === null || _c === void 0 ? void 0 : _c.path) !== null && _g !== void 0 ? _g : "";
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 (_k) {
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 { sendDaemonCommand } = yield Promise.resolve().then(() => __importStar(require("../lib/utils/daemon-client")));
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 sendDaemonCommand({
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
@@ -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>", "Number of matches to show per file", "3")
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)
@@ -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
- query,
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, daemon_client_1.sendDaemonCommand)({
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,
@@ -50,6 +50,7 @@ exports.handleCommand = handleCommand;
50
50
  const fs = __importStar(require("node:fs"));
51
51
  const path = __importStar(require("node:path"));
52
52
  const config_1 = require("../../config");
53
+ const per_file_1 = require("../search/per-file");
53
54
  const logger_1 = require("../utils/logger");
54
55
  const operation_coordinator_1 = require("../utils/operation-coordinator");
55
56
  const path_containment_1 = require("../utils/path-containment");
@@ -153,6 +154,7 @@ function handleCommand(daemon, cmd, conn) {
153
154
  exclusiveGenerationRebuild: exports.EXCLUSIVE_GENERATION_REBUILD_PROTOCOL,
154
155
  readVerbs: read_verbs_1.READ_VERBS_PROTOCOL,
155
156
  searchDiagnostics: 1,
157
+ perFileSearch: 1,
156
158
  // `watch`/`unwatch` accept holder/pid/ttlMs. A daemon without this
157
159
  // treats `unwatch` as unwatch-for-everyone, so clients releasing a
158
160
  // single lease must check it first.
@@ -232,6 +234,13 @@ function handleCommand(daemon, cmd, conn) {
232
234
  conn.on("close", onClose);
233
235
  try {
234
236
  const limitRaw = typeof cmd.limit === "number" ? cmd.limit : 10;
237
+ let maxPerFile;
238
+ try {
239
+ maxPerFile = (0, per_file_1.validateMaxPerFile)(cmd.maxPerFile);
240
+ }
241
+ catch (_m) {
242
+ return { ok: false, error: "invalid maxPerFile" };
243
+ }
235
244
  if (!Number.isInteger(limitRaw) || limitRaw < 1 || limitRaw > 1000) {
236
245
  return { ok: false, error: "invalid limit" };
237
246
  }
@@ -296,6 +305,7 @@ function handleCommand(daemon, cmd, conn) {
296
305
  projectRoot,
297
306
  query,
298
307
  limit: limitRaw,
308
+ maxPerFile,
299
309
  filters,
300
310
  pathPrefix,
301
311
  rerank: cmd.rerank === true,
@@ -45,6 +45,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
45
45
  exports.handleDaemonSearch = handleDaemonSearch;
46
46
  const embedding_generation_1 = require("../index/embedding-generation");
47
47
  const index_state_footer_1 = require("../output/index-state-footer");
48
+ const per_file_1 = require("../search/per-file");
48
49
  const searcher_1 = require("../search/searcher");
49
50
  const retriever_1 = require("../skeleton/retriever");
50
51
  const project_registry_1 = require("../utils/project-registry");
@@ -57,6 +58,13 @@ function handleDaemonSearch(deps, payload, signal) {
57
58
  return __awaiter(this, void 0, void 0, function* () {
58
59
  var _a, _b, _c, _d;
59
60
  var _e, _f, _g;
61
+ let maxPerFile;
62
+ try {
63
+ maxPerFile = (0, per_file_1.validateMaxPerFile)(payload.maxPerFile);
64
+ }
65
+ catch (_h) {
66
+ return { ok: false, error: "invalid maxPerFile" };
67
+ }
60
68
  const { vectorDb, workerPool } = deps;
61
69
  if (!vectorDb || !workerPool) {
62
70
  return { ok: false, error: "daemon not ready" };
@@ -104,6 +112,7 @@ function handleDaemonSearch(deps, payload, signal) {
104
112
  rerank: payload.rerank === true,
105
113
  explain: payload.explain === true,
106
114
  diagnostics: payload.diagnostics === true,
115
+ maxPerFile,
107
116
  seeds: payload.seeds,
108
117
  }, payload.filters, payload.pathPrefix, undefined, signal);
109
118
  }
@@ -155,7 +164,7 @@ function handleDaemonSearch(deps, payload, signal) {
155
164
  if (sk)
156
165
  skeletons[p] = sk;
157
166
  }
158
- catch (_h) {
167
+ catch (_j) {
159
168
  // best-effort — drop the entry, keep the search result
160
169
  }
161
170
  }
@@ -170,7 +179,7 @@ function handleDaemonSearch(deps, payload, signal) {
170
179
  const builder = new GraphBuilder(vectorDb, root);
171
180
  response.graph = yield builder.buildGraphMultiHop(payload.query, 1);
172
181
  }
173
- catch (_j) {
182
+ catch (_k) {
174
183
  // best-effort — drop graph, keep results
175
184
  }
176
185
  }
@@ -48,6 +48,7 @@ const targetSchema = zod_1.z
48
48
  .object({
49
49
  file: zod_1.z.string().min(1),
50
50
  symbol: zod_1.z.string().min(1),
51
+ match: zod_1.z.enum(["symbol-or-range", "range"]).optional(),
51
52
  startLine: zod_1.z.number().int().positive(),
52
53
  endLine: zod_1.z.number().int().positive(),
53
54
  sourceSha256: digest,
@@ -117,14 +118,15 @@ function parseFrozenFixture(bytes, expectedSha256) {
117
118
  }
118
119
  return exports.relevanceFixtureSchema.parse(JSON.parse(bytes.toString("utf8")));
119
120
  }
120
- /** Exact file plus definition symbol OR overlap with a source-grounded line range. */
121
+ /** Exact file plus the frozen matching rule; legacy fixtures allow symbol or range. */
121
122
  function matchesTarget(chunk, root, target) {
122
123
  var _a, _b, _c, _d;
123
124
  if (typeof ((_a = chunk.metadata) === null || _a === void 0 ? void 0 : _a.path) !== "string")
124
125
  return false;
125
126
  if (path.resolve(chunk.metadata.path) !== path.resolve(root, target.file))
126
127
  return false;
127
- if ((_b = chunk.defined_symbols) === null || _b === void 0 ? void 0 : _b.includes(target.symbol))
128
+ if (target.match !== "range" &&
129
+ ((_b = chunk.defined_symbols) === null || _b === void 0 ? void 0 : _b.includes(target.symbol)))
128
130
  return true;
129
131
  const start = (_c = chunk.generated_metadata) === null || _c === void 0 ? void 0 : _c.start_line;
130
132
  const count = (_d = chunk.generated_metadata) === null || _d === void 0 ? void 0 : _d.num_lines;
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.validateMaxPerFile = validateMaxPerFile;
4
+ exports.parseCliPerFile = parseCliPerFile;
5
+ /** Omitted requests retain the daemon's configured default. */
6
+ function validateMaxPerFile(value) {
7
+ if (value === undefined)
8
+ return undefined;
9
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 1)
10
+ throw new Error("maxPerFile must be a positive safe integer");
11
+ return value;
12
+ }
13
+ function parseCliPerFile(value) {
14
+ if (value === undefined)
15
+ return undefined;
16
+ if (!/^\d+$/.test(value))
17
+ throw new Error("--per-file must be a positive safe integer");
18
+ try {
19
+ return validateMaxPerFile(Number(value));
20
+ }
21
+ catch (_a) {
22
+ throw new Error("--per-file must be a positive safe integer");
23
+ }
24
+ }
@@ -21,6 +21,7 @@ const pool_1 = require("../workers/pool");
21
21
  const diagnostics_1 = require("./diagnostics");
22
22
  const intent_1 = require("./intent");
23
23
  const pagerank_1 = require("./pagerank");
24
+ const per_file_1 = require("./per-file");
24
25
  const seed_weight_1 = require("./seed-weight");
25
26
  // Reads a defined_symbols / referenced_symbols column that may arrive as a plain
26
27
  // array or a LanceDB Arrow proxy (.toArray()).
@@ -397,6 +398,7 @@ class Searcher {
397
398
  search(query, top_k, _search_options, _filters, pathPrefix, intent, signal) {
398
399
  return __awaiter(this, void 0, void 0, function* () {
399
400
  var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q;
401
+ const requestedMaxPerFile = (0, per_file_1.validateMaxPerFile)(_search_options === null || _search_options === void 0 ? void 0 : _search_options.maxPerFile);
400
402
  const finalLimit = top_k !== null && top_k !== void 0 ? top_k : 10;
401
403
  // ColBERT rerank is opt-in as of v0.17.1. On the 97-case eval it
402
404
  // regresses MRR@10 by ~3% and doubles query latency; sweep across
@@ -742,7 +744,7 @@ class Searcher {
742
744
  : {}));
743
745
  }
744
746
  const envMaxPerFile = Number.parseInt((_m = process.env.GMAX_MAX_PER_FILE) !== null && _m !== void 0 ? _m : "", 10);
745
- const MAX_PER_FILE = Number.isFinite(envMaxPerFile) && envMaxPerFile > 0 ? envMaxPerFile : 3;
747
+ const MAX_PER_FILE = requestedMaxPerFile !== null && requestedMaxPerFile !== void 0 ? requestedMaxPerFile : (Number.isFinite(envMaxPerFile) && envMaxPerFile > 0 ? envMaxPerFile : 3);
746
748
  // Rank the entire bounded candidate pool before trimming the result count.
747
749
  // A limit-dependent cut here discarded contenders before structure boosts
748
750
  // and dedup, so requesting three results could hide the top match returned
@@ -0,0 +1,34 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.sendSearchCommand = sendSearchCommand;
13
+ const per_file_1 = require("../search/per-file");
14
+ const daemon_client_1 = require("./daemon-client");
15
+ /** An old live daemon must not silently ignore a request-scoped override. */
16
+ function sendSearchCommand(command, options) {
17
+ return __awaiter(this, void 0, void 0, function* () {
18
+ var _a;
19
+ const maxPerFile = (0, per_file_1.validateMaxPerFile)(command.maxPerFile);
20
+ if (maxPerFile !== undefined) {
21
+ const ping = yield (0, daemon_client_1.sendDaemonCommand)({ cmd: "ping" }, options);
22
+ if (!ping.ok)
23
+ return ping;
24
+ if (((_a = ping.capabilities) === null || _a === void 0 ? void 0 : _a.perFileSearch) !== 1) {
25
+ return {
26
+ ok: false,
27
+ error: "unsupported per-file retrieval control",
28
+ hint: "run gmax watch restart with the updated CLI",
29
+ };
30
+ }
31
+ }
32
+ return (0, daemon_client_1.sendDaemonCommand)(command, options);
33
+ });
34
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "grepmax",
3
- "version": "0.26.49",
3
+ "version": "0.26.50",
4
4
  "author": "Robert Owens <78518764+reowens@users.noreply.github.com>",
5
5
  "homepage": "https://github.com/reowens/grepmax",
6
6
  "bugs": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "grepmax",
3
- "version": "0.26.49",
3
+ "version": "0.26.50",
4
4
  "description": "Semantic code search for Claude Code. Automatically indexes your project and provides intelligent search capabilities.",
5
5
  "author": {
6
6
  "name": "Robert Owens",