grepmax 0.17.21 → 0.17.23

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.
@@ -14,6 +14,7 @@ exports.resolveTargetSymbols = resolveTargetSymbols;
14
14
  exports.findTests = findTests;
15
15
  exports.findDependents = findDependents;
16
16
  const filter_builder_1 = require("../utils/filter-builder");
17
+ const query_timeout_1 = require("../utils/query-timeout");
17
18
  const graph_builder_1 = require("./graph-builder");
18
19
  const TEST_DIR_RE = /(^|\/)(__tests__|tests?|specs?|benchmark)(\/|$)/i;
19
20
  const TEST_FILE_RE = /\.(test|spec)\.[cm]?[jt]sx?$/i;
@@ -90,7 +91,11 @@ function expandFileSymbols(symbols, vectorDb, projectRoot, excludePrefixes) {
90
91
  expanded.add(s);
91
92
  }
92
93
  }
93
- return [...expanded];
94
+ // Cap the fan-out: a large class file can define 50+ symbols, and every
95
+ // expanded symbol costs one caller-scan (and more in the fallback). The
96
+ // original target stays first; co-defined symbols are best-effort extras.
97
+ const MAX_EXPANDED = 15;
98
+ return [...expanded].slice(0, MAX_EXPANDED);
94
99
  });
95
100
  }
96
101
  /**
@@ -111,7 +116,7 @@ function findTests(symbols_1, vectorDb_1, projectRoot_1) {
111
116
  yield walkCallers(symbol, graphBuilder, testHits, 0, depth, new Set());
112
117
  }
113
118
  if (testHits.size === 0) {
114
- const importFiles = yield findImportFallbackTests(expanded, vectorDb, projectRoot, excludePrefixes);
119
+ const importFiles = yield findImportFallbackTests(expanded, symbols, vectorDb, projectRoot, excludePrefixes);
115
120
  for (const file of importFiles) {
116
121
  testHits.set(`${file}:(referenced)`, {
117
122
  file,
@@ -124,12 +129,13 @@ function findTests(symbols_1, vectorDb_1, projectRoot_1) {
124
129
  return [...testHits.values()].sort((a, b) => a.hops - b.hops || a.file.localeCompare(b.file));
125
130
  });
126
131
  }
127
- function findImportFallbackTests(symbols, vectorDb, projectRoot, excludePrefixes) {
132
+ function findImportFallbackTests(expandedSymbols, originalSymbols, vectorDb, projectRoot, excludePrefixes) {
128
133
  return __awaiter(this, void 0, void 0, function* () {
129
134
  const files = new Set();
130
135
  // Signal 1: referenced_symbols match (precise; works when the chunker
131
- // captured call references in test bodies).
132
- const dependents = yield findDependents(symbols, vectorDb, projectRoot, undefined, 50, excludePrefixes);
136
+ // captured call references in test bodies). Uses the expanded set so tests
137
+ // that call a method of the target class still match.
138
+ const dependents = yield findDependents(expandedSymbols, vectorDb, projectRoot, undefined, 50, excludePrefixes);
133
139
  for (const d of dependents) {
134
140
  if (isTestPath(d.file))
135
141
  files.add(d.file);
@@ -143,14 +149,18 @@ function findImportFallbackTests(symbols, vectorDb, projectRoot, excludePrefixes
143
149
  const exNorm = ex.endsWith("/") ? ex : `${ex}/`;
144
150
  pathScope += ` AND path NOT LIKE '${(0, filter_builder_1.escapeSqlString)(exNorm)}%'`;
145
151
  }
146
- for (const sym of symbols) {
147
- const rows = yield table
152
+ // Textual matching runs on the ORIGINAL targets only: matching co-defined
153
+ // file symbols (helpers like `log`) textually drags in every test file
154
+ // that mentions them, drowning the answer in false positives.
155
+ for (const sym of originalSymbols) {
156
+ // No .limit() here: LIKE + limit deadlocks in @lancedb 0.27.x when more
157
+ // rows match than the limit (verified). Unlimited scan is fast; cap in JS.
158
+ const rows = yield (0, query_timeout_1.withQueryTimeout)(table
148
159
  .query()
149
160
  .select(["path"])
150
161
  .where(`content LIKE '%${(0, filter_builder_1.escapeSqlString)(sym)}%' AND ${pathScope}`)
151
- .limit(100)
152
- .toArray();
153
- for (const row of rows) {
162
+ .toArray(), `content LIKE %${sym}% (test fallback)`);
163
+ for (const row of rows.slice(0, 500)) {
154
164
  const p = String(row.path || "");
155
165
  if (isTestPath(p))
156
166
  files.add(p);
@@ -198,11 +208,14 @@ function findDependents(symbols_1, vectorDb_1, projectRoot_1, excludePaths_1) {
198
208
  }
199
209
  const counts = new Map();
200
210
  for (const sym of symbols) {
211
+ // 200, not 20: with per-chunk rows a popular symbol easily exceeds 20
212
+ // chunks, and truncation here silently drops whole dependent files.
213
+ // (array_contains + limit does not hit the LIKE+limit native hang.)
201
214
  const rows = yield table
202
215
  .query()
203
216
  .select(["path"])
204
217
  .where(`array_contains(referenced_symbols, '${(0, filter_builder_1.escapeSqlString)(sym)}') AND ${pathScope}`)
205
- .limit(20)
218
+ .limit(200)
206
219
  .toArray();
207
220
  for (const row of rows) {
208
221
  const p = String(row.path || "");
@@ -0,0 +1,54 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.groupTestHitsByFile = groupTestHitsByFile;
4
+ exports.formatViaAgent = formatViaAgent;
5
+ exports.formatViaHuman = formatViaHuman;
6
+ exports.hopLabelAgent = hopLabelAgent;
7
+ exports.hopLabelHuman = hopLabelHuman;
8
+ function groupTestHitsByFile(hits) {
9
+ const byFile = new Map();
10
+ // Sort by hops so the first hit per file carries the best line/hops and
11
+ // `via` lists closest callers first ((referenced) fallback hits sort last).
12
+ const ordered = [...hits].sort((a, b) => (a.hops === -1 ? Number.MAX_SAFE_INTEGER : a.hops) -
13
+ (b.hops === -1 ? Number.MAX_SAFE_INTEGER : b.hops));
14
+ for (const h of ordered) {
15
+ let g = byFile.get(h.file);
16
+ if (!g) {
17
+ g = { file: h.file, line: h.line, hops: h.hops, via: [] };
18
+ byFile.set(h.file, g);
19
+ }
20
+ if (h.symbol && h.symbol !== "(referenced)" && !g.via.includes(h.symbol)) {
21
+ g.via.push(h.symbol);
22
+ }
23
+ }
24
+ return [...byFile.values()].sort((a, b) => (a.hops === -1 ? Number.MAX_SAFE_INTEGER : a.hops) -
25
+ (b.hops === -1 ? Number.MAX_SAFE_INTEGER : b.hops) ||
26
+ a.file.localeCompare(b.file));
27
+ }
28
+ const MAX_VIA = 3;
29
+ /** `via=helperA,helperB(+2)` agent detail, or "" when there is none. */
30
+ function formatViaAgent(via) {
31
+ if (via.length === 0)
32
+ return "";
33
+ const shown = via.slice(0, MAX_VIA).join(",");
34
+ const more = via.length > MAX_VIA ? `(+${via.length - MAX_VIA})` : "";
35
+ return `\tvia=${shown}${more}`;
36
+ }
37
+ /** `, via helperA, helperB (+2 more)` human detail, or "" when none. */
38
+ function formatViaHuman(via) {
39
+ if (via.length === 0)
40
+ return "";
41
+ const shown = via.slice(0, MAX_VIA).join(", ");
42
+ const more = via.length > MAX_VIA ? ` (+${via.length - MAX_VIA} more)` : "";
43
+ return `, via ${shown}${more}`;
44
+ }
45
+ function hopLabelAgent(hops) {
46
+ return hops === -1 ? "via-import" : hops === 0 ? "direct" : `${hops}-hop`;
47
+ }
48
+ function hopLabelHuman(hops) {
49
+ return hops === -1
50
+ ? "via import"
51
+ : hops === 0
52
+ ? "calls directly"
53
+ : `${hops} hop${hops > 1 ? "s" : ""} away`;
54
+ }
@@ -37,6 +37,11 @@ Survey:
37
37
 
38
38
  Scope flags: --root <name|path>, --in <subpath>, --exclude <subpath>.
39
39
  Roles in results: [DEFI] [ORCH] [IMPL] [DOCS].
40
+ Notation: s=/d= score/distance (higher s / lower d = better) · C:n complexity ·
41
+ <- caller / -> callee · dep:/rev: outbound/inbound file deps (count = shared symbols) ·
42
+ test hops: direct = calls it, N-hop = N calls away, via-import = imports but call unseen ·
43
+ test via=X,Y: caller symbols inside the test file (often helpers, not the tests) ·
44
+ (not indexed) = external/builtin, no definition in the index.
40
45
  Recovery: "not added yet" → gmax add; stale → gmax index; broken → gmax doctor --fix.`;
41
46
  /** Full SessionStart context = prefix + cheatsheet. */
42
47
  exports.SESSION_START_HINT = `${exports.SESSION_START_PREFIX}\n\n${exports.AGENT_CHEATSHEET}`;
@@ -811,6 +811,40 @@ class TreeSitterChunker {
811
811
  return { chunks: combined, metadata };
812
812
  });
813
813
  }
814
+ /**
815
+ * Restrict a sub-chunk's symbol lists to names that actually occur in its
816
+ * content slice. `{...chunk}` would otherwise copy the parent's full
817
+ * definedSymbols/referencedSymbols into every sub-chunk, fabricating graph
818
+ * edges: each sub-chunk of a large class "defines" the class and
819
+ * "references" everything the class references, so caller/dependent queries
820
+ * return one phantom hit per sub-chunk (verified: 3 real call sites → 66).
821
+ */
822
+ static scopeSymbolsToContent(sub) {
823
+ const occurs = (s) => {
824
+ if (!s)
825
+ return false;
826
+ if (/^\w+$/.test(s)) {
827
+ return new RegExp(`\\b${s}\\b`).test(sub.content);
828
+ }
829
+ // Names with non-word chars ($foo, a.b) — substring match is the best
830
+ // we can do without a parser; over-keeping is safer than dropping edges.
831
+ return sub.content.includes(s);
832
+ };
833
+ if (sub.definedSymbols) {
834
+ const original = sub.definedSymbols;
835
+ sub.definedSymbols = original.filter(occurs);
836
+ // A mid-body slice defines nothing itself — keep the enclosing
837
+ // definition's name as parentSymbol so graph consumers can attribute
838
+ // the chunk ("Daemon" beats "unknown").
839
+ if (sub.definedSymbols.length === 0 && !sub.parentSymbol && original[0]) {
840
+ sub.parentSymbol = original[0];
841
+ }
842
+ }
843
+ if (sub.referencedSymbols) {
844
+ sub.referencedSymbols = sub.referencedSymbols.filter(occurs);
845
+ }
846
+ return sub;
847
+ }
814
848
  splitIfTooBig(chunk) {
815
849
  const charCount = chunk.content.length;
816
850
  const lines = chunk.content.split("\n");
@@ -830,7 +864,7 @@ class TreeSitterChunker {
830
864
  const subLines = lines.slice(i, end);
831
865
  if (subLines.length < 3 && i > 0)
832
866
  continue;
833
- subChunks.push(Object.assign(Object.assign({}, chunk), { content: subLines.join("\n"), startLine: chunk.startLine + i, endLine: chunk.startLine + end - 1 }));
867
+ subChunks.push(TreeSitterChunker.scopeSymbolsToContent(Object.assign(Object.assign({}, chunk), { content: subLines.join("\n"), startLine: chunk.startLine + i, endLine: chunk.startLine + end - 1 })));
834
868
  }
835
869
  return subChunks.flatMap((sc) => sc.content.length > this.MAX_CHUNK_CHARS ? this.splitByChars(sc) : [sc]);
836
870
  }
@@ -844,7 +878,7 @@ class TreeSitterChunker {
844
878
  continue;
845
879
  const prefixLines = chunk.content.slice(0, i).split("\n").length - 1;
846
880
  const subLineCount = sub.split("\n").length;
847
- res.push(Object.assign(Object.assign({}, chunk), { content: sub, startLine: chunk.startLine + prefixLines, endLine: chunk.startLine + prefixLines + subLineCount - 1 }));
881
+ res.push(TreeSitterChunker.scopeSymbolsToContent(Object.assign(Object.assign({}, chunk), { content: sub, startLine: chunk.startLine + prefixLines, endLine: chunk.startLine + prefixLines + subLineCount - 1 })));
848
882
  }
849
883
  return res;
850
884
  }
@@ -101,11 +101,66 @@ function firstSignatureLine(chunk) {
101
101
  }
102
102
  return "";
103
103
  }
104
- function hintFor(chunk) {
104
+ const QUERY_STOPWORDS = new Set([
105
+ "the", "and", "for", "with", "that", "this", "from", "into", "where",
106
+ "when", "how", "what", "does", "not", "are", "was", "has", "have", "its",
107
+ "all", "can", "use", "uses", "using", "code",
108
+ ]);
109
+ function extractQueryTerms(query) {
110
+ if (!query)
111
+ return [];
112
+ const terms = query
113
+ .toLowerCase()
114
+ .split(/[^a-z0-9_]+/)
115
+ .filter((t) => t.length >= 3 && !QUERY_STOPWORDS.has(t));
116
+ return [...new Set(terms)];
117
+ }
118
+ function truncateHint(line) {
119
+ return line.length > 120 ? `${line.slice(0, 117)}...` : line;
120
+ }
121
+ /**
122
+ * The chunk line that best matches the query terms — it shows *why* this
123
+ * chunk hit, where the first signature line only shows what the chunk is.
124
+ * Most distinct terms wins; code beats comments on a tie; earliest otherwise.
125
+ */
126
+ function queryMatchingLine(chunk, terms) {
127
+ var _a, _b;
128
+ if (!terms.length)
129
+ return "";
130
+ const raw = String((_b = (_a = chunk.content) !== null && _a !== void 0 ? _a : chunk.text) !== null && _b !== void 0 ? _b : "");
131
+ let best = "";
132
+ let bestScore = 0;
133
+ let bestIsComment = true;
134
+ for (const line of raw.split("\n")) {
135
+ const trimmed = line.trim();
136
+ if (!trimmed || trimmed.length < 5)
137
+ continue;
138
+ if (trimmed.startsWith("import ") || trimmed.startsWith("File:"))
139
+ continue;
140
+ if (trimmed === "{" || trimmed === "}")
141
+ continue;
142
+ const lower = trimmed.toLowerCase();
143
+ let score = 0;
144
+ for (const t of terms)
145
+ if (lower.includes(t))
146
+ score++;
147
+ if (score === 0)
148
+ continue;
149
+ const isComment = /^(\/\/|\/\*|\*|#)/.test(trimmed);
150
+ if (score > bestScore ||
151
+ (score === bestScore && bestIsComment && !isComment)) {
152
+ best = trimmed;
153
+ bestScore = score;
154
+ bestIsComment = isComment;
155
+ }
156
+ }
157
+ return truncateHint(best);
158
+ }
159
+ function hintFor(chunk, queryTerms) {
105
160
  if (typeof chunk.summary === "string" && chunk.summary) {
106
161
  return chunk.summary;
107
162
  }
108
- return firstSignatureLine(chunk);
163
+ return queryMatchingLine(chunk, queryTerms) || firstSignatureLine(chunk);
109
164
  }
110
165
  function explainSuffix(chunk, enabled) {
111
166
  if (!enabled || !chunk.scoreBreakdown)
@@ -117,6 +172,7 @@ function formatAgentSearchResults(results, projectRoot, options = {}) {
117
172
  var _a, _b;
118
173
  if (!results.length)
119
174
  return "(none)";
175
+ const queryTerms = extractQueryTerms(options.query);
120
176
  const groups = new Map();
121
177
  for (const result of results) {
122
178
  const absPath = chunkPath(result);
@@ -152,7 +208,7 @@ function formatAgentSearchResults(results, projectRoot, options = {}) {
152
208
  const score = typeof result.score === "number"
153
209
  ? `\ts=${result.score.toFixed(3)}`
154
210
  : "";
155
- const hint = hintFor(result);
211
+ const hint = hintFor(result, queryTerms);
156
212
  const locator = grouped
157
213
  ? ` :${chunkStartLine(result)}`
158
214
  : `${rel}:${chunkStartLine(result)}`;
@@ -0,0 +1,244 @@
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.toTextResults = toTextResults;
37
+ exports.toCompactHits = toCompactHits;
38
+ exports.formatCompactTable = formatCompactTable;
39
+ exports.resultCountHeader = resultCountHeader;
40
+ const path = __importStar(require("node:path"));
41
+ function toTextResults(data) {
42
+ return data.map((r) => {
43
+ var _a, _b, _c, _d, _e, _f;
44
+ const rawPath = typeof ((_a = r.metadata) === null || _a === void 0 ? void 0 : _a.path) === "string"
45
+ ? r.metadata.path
46
+ : "Unknown path";
47
+ const start = typeof ((_b = r.generated_metadata) === null || _b === void 0 ? void 0 : _b.start_line) === "number"
48
+ ? r.generated_metadata.start_line
49
+ : 0;
50
+ const end = typeof ((_c = r.generated_metadata) === null || _c === void 0 ? void 0 : _c.end_line) === "number"
51
+ ? r.generated_metadata.end_line
52
+ : start + Math.max(0, ((_e = (_d = r.generated_metadata) === null || _d === void 0 ? void 0 : _d.num_lines) !== null && _e !== void 0 ? _e : 1) - 1);
53
+ return {
54
+ path: rawPath,
55
+ score: r.score,
56
+ content: r.text || "",
57
+ chunk_type: (_f = r.generated_metadata) === null || _f === void 0 ? void 0 : _f.type,
58
+ start_line: start,
59
+ end_line: end,
60
+ };
61
+ });
62
+ }
63
+ function getPreviewText(chunk) {
64
+ var _a, _b, _c, _d, _e;
65
+ const maxLen = 140;
66
+ const lines = (_b = (_a = chunk.text) === null || _a === void 0 ? void 0 : _a.split("\n").map((l) => l.trim()).filter(Boolean)) !== null && _b !== void 0 ? _b : [];
67
+ let preview = (_c = lines[0]) !== null && _c !== void 0 ? _c : "";
68
+ if (!preview && ((_d = chunk.defined_symbols) === null || _d === void 0 ? void 0 : _d.length)) {
69
+ preview = (_e = chunk.defined_symbols[0]) !== null && _e !== void 0 ? _e : "";
70
+ }
71
+ if (preview.length > maxLen) {
72
+ preview = `${preview.slice(0, maxLen)}...`;
73
+ }
74
+ return preview;
75
+ }
76
+ function toCompactHits(data) {
77
+ return data.map((chunk) => {
78
+ var _a, _b, _c, _d, _e, _f;
79
+ const rawPath = typeof ((_a = chunk.metadata) === null || _a === void 0 ? void 0 : _a.path) === "string"
80
+ ? chunk.metadata.path
81
+ : "Unknown path";
82
+ const start = typeof ((_b = chunk.generated_metadata) === null || _b === void 0 ? void 0 : _b.start_line) === "number"
83
+ ? chunk.generated_metadata.start_line
84
+ : 0;
85
+ const end = typeof ((_c = chunk.generated_metadata) === null || _c === void 0 ? void 0 : _c.end_line) === "number"
86
+ ? chunk.generated_metadata.end_line
87
+ : start + Math.max(0, ((_e = (_d = chunk.generated_metadata) === null || _d === void 0 ? void 0 : _d.num_lines) !== null && _e !== void 0 ? _e : 1) - 1);
88
+ return {
89
+ path: rawPath,
90
+ range: `${start + 1}-${end + 1}`,
91
+ start_line: start,
92
+ end_line: end,
93
+ role: chunk.role,
94
+ confidence: chunk.confidence,
95
+ score: chunk.score,
96
+ defined: Array.isArray(chunk.defined_symbols)
97
+ ? chunk.defined_symbols.slice(0, 3)
98
+ : typeof chunk.defined_symbols === "string"
99
+ ? [chunk.defined_symbols]
100
+ : typeof ((_f = chunk.defined_symbols) === null || _f === void 0 ? void 0 : _f.toArray) === "function"
101
+ ? chunk.defined_symbols.toArray().slice(0, 3)
102
+ : [],
103
+ preview: getPreviewText(chunk),
104
+ summary: typeof chunk.summary === "string" ? chunk.summary : undefined,
105
+ };
106
+ });
107
+ }
108
+ function compactRole(role) {
109
+ if (!role)
110
+ return "UNK";
111
+ if (role.startsWith("ORCH"))
112
+ return "ORCH";
113
+ if (role.startsWith("DEF"))
114
+ return "DEF";
115
+ if (role.startsWith("IMP"))
116
+ return "IMPL";
117
+ return role.slice(0, 4).toUpperCase();
118
+ }
119
+ function compactConf(conf) {
120
+ if (!conf)
121
+ return "U";
122
+ const c = conf.toUpperCase();
123
+ if (c.startsWith("H"))
124
+ return "H";
125
+ if (c.startsWith("M"))
126
+ return "M";
127
+ if (c.startsWith("L"))
128
+ return "L";
129
+ return "U";
130
+ }
131
+ function compactScore(score) {
132
+ if (typeof score !== "number")
133
+ return "";
134
+ const fixed = score.toFixed(3);
135
+ return fixed
136
+ .replace(/^0\./, ".")
137
+ .replace(/\.?0+$/, (m) => (m.startsWith(".") ? "" : m));
138
+ }
139
+ function truncateEnd(s, max) {
140
+ if (max <= 0)
141
+ return "";
142
+ if (s.length <= max)
143
+ return s;
144
+ if (max <= 3)
145
+ return s.slice(0, max);
146
+ return `${s.slice(0, max - 3)}...`;
147
+ }
148
+ function padR(s, w) {
149
+ const n = Math.max(0, w - s.length);
150
+ return s + " ".repeat(n);
151
+ }
152
+ function padL(s, w) {
153
+ const n = Math.max(0, w - s.length);
154
+ return " ".repeat(n) + s;
155
+ }
156
+ function formatCompactTSV(hits, projectRoot, query) {
157
+ var _a, _b;
158
+ if (!hits.length)
159
+ return "No matches found.";
160
+ const lines = [];
161
+ lines.push(`gmax hits\tquery=${query}\tcount=${hits.length}`);
162
+ lines.push("path\tlines\tscore\trole\tconf\tdefined\tsummary");
163
+ for (const hit of hits) {
164
+ const relPath = path.isAbsolute(hit.path)
165
+ ? path.relative(projectRoot, hit.path)
166
+ : hit.path;
167
+ const score = compactScore(hit.score);
168
+ const role = compactRole(hit.role);
169
+ const conf = compactConf(hit.confidence);
170
+ const defs = ((_a = hit.defined) !== null && _a !== void 0 ? _a : []).join(",");
171
+ const summary = (_b = hit.summary) !== null && _b !== void 0 ? _b : "";
172
+ lines.push([relPath, hit.range, score, role, conf, defs, summary].join("\t"));
173
+ }
174
+ return lines.join("\n");
175
+ }
176
+ function formatCompactPretty(hits, projectRoot, query, termWidth, useAnsi) {
177
+ var _a;
178
+ if (!hits.length)
179
+ return "No matches found.";
180
+ const dim = (s) => (useAnsi ? `\x1b[90m${s}\x1b[0m` : s);
181
+ const bold = (s) => (useAnsi ? `\x1b[1m${s}\x1b[0m` : s);
182
+ const wLines = 9;
183
+ const wScore = 6;
184
+ const wRole = 4;
185
+ const wConf = 1;
186
+ const wDef = 20;
187
+ const gutters = 5;
188
+ const fixed = wLines + wScore + wRole + wConf + wDef + gutters;
189
+ const wPath = Math.max(24, Math.min(64, termWidth - fixed));
190
+ const header = `gmax hits count=${hits.length} query="${query}"`;
191
+ const cols = [
192
+ padR("path", wPath),
193
+ padR("lines", wLines),
194
+ padL("score", wScore),
195
+ padR("role", wRole),
196
+ padR("c", wConf),
197
+ padR("defined", wDef),
198
+ ].join(" ");
199
+ const out = [];
200
+ out.push(bold(header));
201
+ out.push(dim(cols));
202
+ for (const hit of hits) {
203
+ const relPath = path.isAbsolute(hit.path)
204
+ ? path.relative(projectRoot, hit.path)
205
+ : hit.path;
206
+ const score = compactScore(hit.score);
207
+ const role = compactRole(hit.role);
208
+ const conf = compactConf(hit.confidence);
209
+ const defs = ((_a = hit.defined) !== null && _a !== void 0 ? _a : []).join(",") || "-";
210
+ const displayPath = `${relPath}:${hit.start_line + 1}`;
211
+ const paddedPath = padR(displayPath, wPath);
212
+ const row = [
213
+ paddedPath,
214
+ padR(hit.range, wLines),
215
+ padL(score || "", wScore),
216
+ padR(role, wRole),
217
+ padR(conf, wConf),
218
+ padR(truncateEnd(defs, wDef), wDef),
219
+ ].join(" ");
220
+ out.push(row);
221
+ }
222
+ return out.join("\n");
223
+ }
224
+ function formatCompactTable(hits, projectRoot, query, opts) {
225
+ var _a;
226
+ if (!hits.length)
227
+ return "No matches found.";
228
+ if (!opts.isTTY || opts.plain) {
229
+ return formatCompactTSV(hits, projectRoot, query);
230
+ }
231
+ const termWidth = Math.max(80, (_a = process.stdout.columns) !== null && _a !== void 0 ? _a : 120);
232
+ return formatCompactPretty(hits, projectRoot, query, termWidth, true);
233
+ }
234
+ function resultCountHeader(results, maxCount) {
235
+ var _a, _b, _c;
236
+ const files = new Set();
237
+ for (const r of results) {
238
+ const p = (_c = (_a = r.path) !== null && _a !== void 0 ? _a : (_b = r.metadata) === null || _b === void 0 ? void 0 : _b.path) !== null && _c !== void 0 ? _c : "";
239
+ if (p)
240
+ files.add(p);
241
+ }
242
+ const showing = results.length < maxCount ? `${results.length}` : `top ${results.length}`;
243
+ return `Found ${results.length} match${results.length === 1 ? "" : "es"} (showing ${showing}) across ${files.size} file${files.size === 1 ? "" : "s"}`;
244
+ }
@@ -817,7 +817,7 @@ class Searcher {
817
817
  const displayRows = yield table
818
818
  .query()
819
819
  .select([
820
- "id", "defined_symbols", "imports", "exports",
820
+ "id", "defined_symbols", "parent_symbol", "imports", "exports",
821
821
  "summary", "file_skeleton",
822
822
  ])
823
823
  .where(`id IN (${finalIds.join(",")})`)
@@ -828,6 +828,7 @@ class Searcher {
828
828
  const extra = displayMap.get(item.record.id);
829
829
  if (extra) {
830
830
  item.record.defined_symbols = extra.defined_symbols;
831
+ item.record.parent_symbol = extra.parent_symbol;
831
832
  item.record.imports = extra.imports;
832
833
  item.record.exports = extra.exports;
833
834
  item.record.summary = extra.summary;
@@ -0,0 +1,52 @@
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.QueryTimeoutError = void 0;
13
+ exports.withQueryTimeout = withQueryTimeout;
14
+ const DEFAULT_TIMEOUT_MS = Number(process.env.GMAX_QUERY_TIMEOUT_MS || "") || 15000;
15
+ class QueryTimeoutError extends Error {
16
+ constructor(label, ms) {
17
+ super(`LanceDB query timed out after ${ms}ms (${label}). ` +
18
+ `The store may be busy or hitting a native scan bug — retry, or run: gmax doctor`);
19
+ this.name = "QueryTimeoutError";
20
+ }
21
+ }
22
+ exports.QueryTimeoutError = QueryTimeoutError;
23
+ /**
24
+ * Race a LanceDB query against a wall-clock timeout so a native-layer deadlock
25
+ * surfaces as a loud error instead of hanging the process forever.
26
+ *
27
+ * Known trigger (@lancedb/lancedb 0.27.x): a `content LIKE` scan with
28
+ * `.limit(N)` where more than N rows match never resolves — the limit-pushdown
29
+ * cancellation loses the completion (see lancedb/lancedb#2189 for the same
30
+ * family of hangs). Callers should also avoid that query shape (scan without
31
+ * a limit and cap in JS); this wrapper is the backstop for shapes we missed.
32
+ *
33
+ * The timed-out native promise is NOT cancelled — its tokio task may stay
34
+ * parked. CLI commands exit via gracefulExit() (process.exit), so the leak is
35
+ * bounded to the command's lifetime. Long-lived callers (daemon) should treat
36
+ * a QueryTimeoutError as a signal that the connection may be wedged.
37
+ */
38
+ function withQueryTimeout(promise_1, label_1) {
39
+ return __awaiter(this, arguments, void 0, function* (promise, label, ms = DEFAULT_TIMEOUT_MS) {
40
+ let timer;
41
+ const timeout = new Promise((_, reject) => {
42
+ timer = setTimeout(() => reject(new QueryTimeoutError(label, ms)), ms);
43
+ });
44
+ try {
45
+ return yield Promise.race([promise, timeout]);
46
+ }
47
+ finally {
48
+ if (timer)
49
+ clearTimeout(timer);
50
+ }
51
+ });
52
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "grepmax",
3
- "version": "0.17.21",
3
+ "version": "0.17.23",
4
4
  "author": "Robert Owens <78518764+reowens@users.noreply.github.com>",
5
5
  "homepage": "https://github.com/reowens/grepmax",
6
6
  "bugs": {
@@ -59,7 +59,7 @@
59
59
  "dependencies": {
60
60
  "@clack/prompts": "^1.1.0",
61
61
  "@huggingface/transformers": "^4.0.0",
62
- "@lancedb/lancedb": "^0.27.1",
62
+ "@lancedb/lancedb": "^0.30.0",
63
63
  "@modelcontextprotocol/sdk": "^1.29.0",
64
64
  "@parcel/watcher": "^2.5.6",
65
65
  "apache-arrow": "^18.1.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "grepmax",
3
- "version": "0.17.21",
3
+ "version": "0.17.23",
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",