grepmax 0.26.48 → 0.26.50

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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;
@@ -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
+ }
@@ -18,8 +18,10 @@ 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");
24
+ const per_file_1 = require("./per-file");
23
25
  const seed_weight_1 = require("./seed-weight");
24
26
  // Reads a defined_symbols / referenced_symbols column that may arrive as a plain
25
27
  // array or a LanceDB Arrow proxy (.toArray()).
@@ -396,6 +398,7 @@ class Searcher {
396
398
  search(query, top_k, _search_options, _filters, pathPrefix, intent, signal) {
397
399
  return __awaiter(this, void 0, void 0, function* () {
398
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);
399
402
  const finalLimit = top_k !== null && top_k !== void 0 ? top_k : 10;
400
403
  // ColBERT rerank is opt-in as of v0.17.1. On the 97-case eval it
401
404
  // regresses MRR@10 by ~3% and doubles query latency; sweep across
@@ -403,6 +406,14 @@ class Searcher {
403
406
  // fused scores ~30:1 so blend tuning can't recover the loss.
404
407
  let doRerank = (_a = _search_options === null || _search_options === void 0 ? void 0 : _search_options.rerank) !== null && _a !== void 0 ? _a : false;
405
408
  const explain = (_b = _search_options === null || _search_options === void 0 ? void 0 : _search_options.explain) !== null && _b !== void 0 ? _b : false;
409
+ const diagnostics = (_search_options === null || _search_options === void 0 ? void 0 : _search_options.diagnostics)
410
+ ? new diagnostics_1.SearchDiagnosticCollector()
411
+ : undefined;
412
+ const requestedRerank = doRerank;
413
+ let gateEvaluated = false;
414
+ let gateShare = null;
415
+ let gateActivated = false;
416
+ let rerankInvoked = false;
406
417
  // Aider-style seeding (Phase 4): bias candidate generation toward the
407
418
  // agent's working context. Inert unless the caller supplied seed files or
408
419
  // symbols, so the default search path is unchanged.
@@ -569,6 +580,9 @@ class Searcher {
569
580
  .sort((a, b) => b[1] - a[1])
570
581
  .map(([key]) => docMap.get(key))
571
582
  .filter(Boolean);
583
+ diagnostics === null || diagnostics === void 0 ? void 0 : diagnostics.capture("vector", vectorResults);
584
+ diagnostics === null || diagnostics === void 0 ? void 0 : diagnostics.capture("fts", ftsResults);
585
+ diagnostics === null || diagnostics === void 0 ? void 0 : diagnostics.capture("rrf", fused);
572
586
  // Free raw search results — docMap holds the only needed references
573
587
  vectorResults.length = 0;
574
588
  ftsResults.length = 0;
@@ -618,16 +632,16 @@ class Searcher {
618
632
  // pools (express, platform) it regresses. So detect the concentrated regime
619
633
  // here and *add* rerank-on for it. This only ever flips doRerank false→true:
620
634
  // an explicit GMAX_RERANK=1 (doRerank already true) is never overridden off.
635
+ const envConcThreshold = Number.parseFloat((_f = process.env.GMAX_CONCENTRATION_THRESHOLD) !== null && _f !== void 0 ? _f : "");
636
+ // <= 0 (or NaN with the default) keeps the gate active at 0.7; a value > 1
637
+ // disables it (no possible share reaches it), giving a rerank-fully-off
638
+ // baseline for sweeps without touching the doRerank default. 0.7 is the
639
+ // sweep winner: highest threshold (least spurious firing) that still
640
+ // retains lodash's +0.15 MRR lift while leaving express/platform flat.
641
+ const CONCENTRATION_THRESHOLD = Number.isFinite(envConcThreshold) && envConcThreshold > 0
642
+ ? envConcThreshold
643
+ : 0.7;
621
644
  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
645
  // Histogram a fixed top-K window (not finalLimit) so the threshold stays
632
646
  // calibrated across callers regardless of the result window they request.
633
647
  const CONCENTRATION_K = 10;
@@ -643,16 +657,21 @@ class Searcher {
643
657
  maxBucket = count;
644
658
  }
645
659
  const share = maxBucket / window.length;
660
+ gateEvaluated = true;
661
+ gateShare = share;
646
662
  if (share >= CONCENTRATION_THRESHOLD) {
647
663
  doRerank = true;
664
+ gateActivated = true;
648
665
  }
649
666
  }
650
667
  }
668
+ diagnostics === null || diagnostics === void 0 ? void 0 : diagnostics.capture("fusion", fused);
651
669
  // Item 8: Widen PRE_RERANK_K
652
670
  // Retrieve a wide set for Stage 1 filtering
653
671
  const envStage1 = Number.parseInt((_h = process.env.GMAX_STAGE1_K) !== null && _h !== void 0 ? _h : "", 10);
654
672
  const STAGE1_K = Number.isFinite(envStage1) && envStage1 > 0 ? envStage1 : 200;
655
673
  const topCandidates = fused.slice(0, STAGE1_K);
674
+ diagnostics === null || diagnostics === void 0 ? void 0 : diagnostics.capture("stage1", topCandidates);
656
675
  // Free docMap — topCandidates already holds record references
657
676
  docMap.clear();
658
677
  // Item 9: Two-stage rerank
@@ -694,11 +713,38 @@ class Searcher {
694
713
  withScore.sort((a, b) => b.score - a.score);
695
714
  stage2Candidates = withScore.slice(0, STAGE2_K).map((x) => x.doc);
696
715
  }
716
+ diagnostics === null || diagnostics === void 0 ? void 0 : diagnostics.capture("pooled", stage2Candidates);
717
+ const diagnosticDetails = () => ({
718
+ settings: {
719
+ finalLimit,
720
+ preK: PRE_RERANK_K,
721
+ rrfK: RRF_K,
722
+ stage1K: STAGE1_K,
723
+ stage2K: STAGE2_K,
724
+ rerankTop: RERANK_TOP,
725
+ fusedWeight: FUSED_WEIGHT,
726
+ pagerankEnabled,
727
+ seedsActive: seedCtx.active,
728
+ symbolQuery,
729
+ pooledFilterApplied: !!queryPooled && topCandidates.length > STAGE2_K,
730
+ },
731
+ gate: {
732
+ requestedRerank,
733
+ threshold: CONCENTRATION_THRESHOLD,
734
+ evaluated: gateEvaluated,
735
+ share: gateShare,
736
+ activated: gateActivated,
737
+ rerankInvoked,
738
+ },
739
+ fts: { available: this.ftsAvailable, searchFailed: ftsSearchFailed },
740
+ });
697
741
  if (stage2Candidates.length === 0) {
698
- return { data: [] };
742
+ return Object.assign({ data: [] }, (diagnostics
743
+ ? { diagnostics: diagnostics.finish(diagnosticDetails()) }
744
+ : {}));
699
745
  }
700
746
  const envMaxPerFile = Number.parseInt((_m = process.env.GMAX_MAX_PER_FILE) !== null && _m !== void 0 ? _m : "", 10);
701
- 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);
702
748
  // Rank the entire bounded candidate pool before trimming the result count.
703
749
  // A limit-dependent cut here discarded contenders before structure boosts
704
750
  // and dedup, so requesting three results could hide the top match returned
@@ -735,6 +781,8 @@ class Searcher {
735
781
  }
736
782
  }
737
783
  }
784
+ // Selected rerank membership is separate from actual batch execution.
785
+ diagnostics === null || diagnostics === void 0 ? void 0 : diagnostics.capture("rerank", rerankCandidates);
738
786
  // Phase B: Lazy-load colbert data only for the ~20 rerank candidates
739
787
  if (doRerank && rerankCandidates.length > 0) {
740
788
  const rerankIds = rerankCandidates
@@ -761,6 +809,7 @@ class Searcher {
761
809
  }
762
810
  const rerankScoreByKey = new Map();
763
811
  if (doRerank && rerankCandidates.length > 0) {
812
+ rerankInvoked = true;
764
813
  const scores = yield pool.rerank({
765
814
  query: queryMatrixRaw,
766
815
  docs: rerankCandidates.map((doc) => { var _a; return ({
@@ -847,8 +896,10 @@ class Searcher {
847
896
  }
848
897
  // Note: "boosted" was not previously declared -- fix to use "scored"
849
898
  scored.sort((a, b) => b.score - a.score);
899
+ diagnostics === null || diagnostics === void 0 ? void 0 : diagnostics.capture("scored", scored.map((item) => item.record));
850
900
  // Item 11: Intelligent Deduplication
851
901
  const uniqueScored = this.deduplicateResults(scored);
902
+ diagnostics === null || diagnostics === void 0 ? void 0 : diagnostics.capture("dedup", uniqueScored.map((item) => item.record));
852
903
  // Item 10: Per-file diversification
853
904
  const seenFiles = new Map();
854
905
  const diversified = [];
@@ -859,9 +910,13 @@ class Searcher {
859
910
  diversified.push(item);
860
911
  seenFiles.set(path, count + 1);
861
912
  }
913
+ else {
914
+ diagnostics === null || diagnostics === void 0 ? void 0 : diagnostics.noteFileLimit(item.record);
915
+ }
862
916
  if (diversified.length >= finalLimit)
863
917
  break;
864
918
  }
919
+ diagnostics === null || diagnostics === void 0 ? void 0 : diagnostics.capture("final", diversified.map((item) => item.record));
865
920
  // Phase C: Lazy-load display columns only for the final ~10 results
866
921
  const finalIds = diversified
867
922
  .map((item) => item.record.id)
@@ -898,7 +953,7 @@ class Searcher {
898
953
  const finalResults = diversified.map((item) => (Object.assign(Object.assign({}, item.record), { _score: item.score, _breakdown: item.breakdown, vector: undefined, colbert: undefined })));
899
954
  // Item 12: Score Calibration
900
955
  const maxScore = finalResults.length > 0 ? finalResults[0]._score : 1.0;
901
- return Object.assign({ data: finalResults.map((r) => {
956
+ return Object.assign(Object.assign({ data: finalResults.map((r) => {
902
957
  const chunk = this.mapRecordToChunk(r, r._score || 0);
903
958
  // Normalize score relative to top result
904
959
  const normalized = maxScore > 0 ? r._score / maxScore : 0;
@@ -913,7 +968,11 @@ class Searcher {
913
968
  }
914
969
  chunk.confidence = confidence;
915
970
  return chunk;
916
- }) }, (!this.ftsAvailable || ftsSearchFailed
971
+ }) }, (diagnostics
972
+ ? {
973
+ diagnostics: diagnostics.finish(Object.assign(Object.assign({}, diagnosticDetails()), { settings: Object.assign(Object.assign({}, diagnosticDetails().settings), { maxPerFile: MAX_PER_FILE, definitionBoost: DEF_MATCH_BOOST }) })),
974
+ }
975
+ : {})), (!this.ftsAvailable || ftsSearchFailed
917
976
  ? {
918
977
  warnings: [
919
978
  "Full-text search unavailable — results may be less precise",
@@ -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.48",
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": {
@@ -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, LLM summaries, call graph tracing.",
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",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "grepmax",
3
- "version": "0.26.48",
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",