grepmax 0.16.3 → 0.16.5
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/dist/commands/extract.js +20 -0
- package/dist/commands/peek.js +19 -0
- package/dist/commands/related.js +73 -1
- package/dist/commands/search.js +71 -50
- package/dist/commands/test-find.js +10 -2
- package/dist/commands/trace.js +141 -4
- package/dist/lib/graph/impact.js +51 -0
- package/dist/lib/utils/tests-footer.js +78 -0
- package/package.json +1 -1
- package/plugins/grepmax/.claude-plugin/plugin.json +1 -1
- package/plugins/grepmax/hooks/start.js +22 -2
- package/plugins/grepmax/hooks/subagent-start.js +1 -1
- package/plugins/grepmax/skills/grepmax/SKILL.md +11 -2
package/dist/commands/extract.js
CHANGED
|
@@ -110,6 +110,7 @@ exports.extract = new commander_1.Command("extract")
|
|
|
110
110
|
.option("--exclude <subpath>", "Exclude a sub-path of the project (repeatable)", (value, prev) => (prev ? [...prev, value] : [value]))
|
|
111
111
|
.option("--agent", "Compact output for AI agents", false)
|
|
112
112
|
.option("--imports", "Prepend file imports", false)
|
|
113
|
+
.option("--no-tests", "Suppress the tests footer")
|
|
113
114
|
.action((symbol, opts) => __awaiter(void 0, void 0, void 0, function* () {
|
|
114
115
|
var _a;
|
|
115
116
|
let vectorDb = null;
|
|
@@ -176,6 +177,16 @@ exports.extract = new commander_1.Command("extract")
|
|
|
176
177
|
}
|
|
177
178
|
console.log(`${relPath}:${startLine + 1}-${endLine + 1}`);
|
|
178
179
|
console.log(body.join("\n"));
|
|
180
|
+
if (opts.tests !== false) {
|
|
181
|
+
const { fetchTestsForFooter, renderTestsFooterAgent } = yield Promise.resolve().then(() => __importStar(require("../lib/utils/tests-footer")));
|
|
182
|
+
const tests = yield fetchTestsForFooter(symbol, vectorDb, scope.pathPrefix, scope.excludePrefixes);
|
|
183
|
+
if (tests && tests.length > 0) {
|
|
184
|
+
console.log("--- tests:");
|
|
185
|
+
for (const line of renderTestsFooterAgent(tests, projectRoot)) {
|
|
186
|
+
console.log(line);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
179
190
|
}
|
|
180
191
|
else {
|
|
181
192
|
// Rich output with line numbers
|
|
@@ -207,6 +218,15 @@ exports.extract = new commander_1.Command("extract")
|
|
|
207
218
|
.join(", ");
|
|
208
219
|
console.log(`\n${style.dim(`Also defined in: ${otherLocs}`)}`);
|
|
209
220
|
}
|
|
221
|
+
if (!opts.agent && opts.tests !== false) {
|
|
222
|
+
const { fetchTestsForFooter, renderTestsFooterHuman } = yield Promise.resolve().then(() => __importStar(require("../lib/utils/tests-footer")));
|
|
223
|
+
const tests = yield fetchTestsForFooter(symbol, vectorDb, scope.pathPrefix, scope.excludePrefixes);
|
|
224
|
+
if (tests && tests.length > 0) {
|
|
225
|
+
for (const line of renderTestsFooterHuman(tests, projectRoot)) {
|
|
226
|
+
console.log(line);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
210
230
|
}
|
|
211
231
|
catch (error) {
|
|
212
232
|
const message = error instanceof Error ? error.message : "Unknown error";
|
package/dist/commands/peek.js
CHANGED
|
@@ -98,6 +98,7 @@ exports.peek = new commander_1.Command("peek")
|
|
|
98
98
|
.option("--in <subpath>", "Restrict to a sub-path of the project (repeatable)", (value, prev) => (prev ? [...prev, value] : [value]))
|
|
99
99
|
.option("--exclude <subpath>", "Exclude a sub-path of the project (repeatable)", (value, prev) => (prev ? [...prev, value] : [value]))
|
|
100
100
|
.option("--agent", "Compact output for AI agents", false)
|
|
101
|
+
.option("--no-tests", "Suppress the tests footer")
|
|
101
102
|
.action((symbol, opts) => __awaiter(void 0, void 0, void 0, function* () {
|
|
102
103
|
var _a;
|
|
103
104
|
let vectorDb = null;
|
|
@@ -225,6 +226,15 @@ exports.peek = new commander_1.Command("peek")
|
|
|
225
226
|
if (calleeList.length > MAX_CALLEES) {
|
|
226
227
|
console.log(`-> ... ${calleeList.length - MAX_CALLEES} more`);
|
|
227
228
|
}
|
|
229
|
+
if (opts.tests !== false) {
|
|
230
|
+
const { fetchTestsForFooter, renderTestsFooterAgent } = yield Promise.resolve().then(() => __importStar(require("../lib/utils/tests-footer")));
|
|
231
|
+
const tests = yield fetchTestsForFooter(symbol, vectorDb, scope.pathPrefix, scope.excludePrefixes);
|
|
232
|
+
if (tests && tests.length > 0) {
|
|
233
|
+
for (const line of renderTestsFooterAgent(tests, projectRoot)) {
|
|
234
|
+
console.log(line);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
228
238
|
}
|
|
229
239
|
else {
|
|
230
240
|
// Rich output
|
|
@@ -276,6 +286,15 @@ exports.peek = new commander_1.Command("peek")
|
|
|
276
286
|
else {
|
|
277
287
|
console.log(style.dim("No known callees."));
|
|
278
288
|
}
|
|
289
|
+
if (opts.tests !== false) {
|
|
290
|
+
const { fetchTestsForFooter, renderTestsFooterHuman } = yield Promise.resolve().then(() => __importStar(require("../lib/utils/tests-footer")));
|
|
291
|
+
const tests = yield fetchTestsForFooter(symbol, vectorDb, scope.pathPrefix, scope.excludePrefixes);
|
|
292
|
+
if (tests && tests.length > 0) {
|
|
293
|
+
for (const line of renderTestsFooterHuman(tests, projectRoot)) {
|
|
294
|
+
console.log(line);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
279
298
|
}
|
|
280
299
|
}
|
|
281
300
|
catch (error) {
|
package/dist/commands/related.js
CHANGED
|
@@ -138,6 +138,52 @@ exports.related = new commander_1.Command("related")
|
|
|
138
138
|
const topRevs = Array.from(revCounts.entries())
|
|
139
139
|
.sort((a, b) => b[1] - a[1])
|
|
140
140
|
.slice(0, limit);
|
|
141
|
+
// Mention-based fallback: when symbol intersection turns up nothing
|
|
142
|
+
// in either direction, look for files in scope whose content mentions
|
|
143
|
+
// this file's basename. Catches leaf modules with no shared symbols
|
|
144
|
+
// and side-effect-only imports.
|
|
145
|
+
const GENERIC_BASENAMES = new Set([
|
|
146
|
+
"index",
|
|
147
|
+
"main",
|
|
148
|
+
"mod",
|
|
149
|
+
"init",
|
|
150
|
+
"lib",
|
|
151
|
+
"types",
|
|
152
|
+
"util",
|
|
153
|
+
"utils",
|
|
154
|
+
"common",
|
|
155
|
+
"shared",
|
|
156
|
+
]);
|
|
157
|
+
let mentions = [];
|
|
158
|
+
let basename = "";
|
|
159
|
+
let basenameRejected = false;
|
|
160
|
+
if (topDeps.length === 0 && topRevs.length === 0) {
|
|
161
|
+
const ext = path.extname(absPath);
|
|
162
|
+
basename = path.basename(absPath, ext);
|
|
163
|
+
if (basename.length < 4 || GENERIC_BASENAMES.has(basename.toLowerCase())) {
|
|
164
|
+
basenameRejected = true;
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
const rows = yield table
|
|
168
|
+
.query()
|
|
169
|
+
.select(["path"])
|
|
170
|
+
.where(`content LIKE '%${(0, filter_builder_1.escapeSqlString)(basename)}%' AND ${pathScope}`)
|
|
171
|
+
.limit(limit * 4)
|
|
172
|
+
.toArray();
|
|
173
|
+
const seen = new Set();
|
|
174
|
+
for (const row of rows) {
|
|
175
|
+
const p = String(row.path || "");
|
|
176
|
+
if (!p || p === absPath)
|
|
177
|
+
continue;
|
|
178
|
+
if (seen.has(p))
|
|
179
|
+
continue;
|
|
180
|
+
seen.add(p);
|
|
181
|
+
mentions.push(p);
|
|
182
|
+
if (mentions.length >= limit)
|
|
183
|
+
break;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
141
187
|
if (opts.agent) {
|
|
142
188
|
const rel = (p) => p.startsWith(`${projectRoot}/`)
|
|
143
189
|
? p.slice(projectRoot.length + 1)
|
|
@@ -149,7 +195,18 @@ exports.related = new commander_1.Command("related")
|
|
|
149
195
|
console.log(`rev: ${rel(p)}\t${count}`);
|
|
150
196
|
}
|
|
151
197
|
if (!topDeps.length && !topRevs.length) {
|
|
152
|
-
|
|
198
|
+
if (basenameRejected) {
|
|
199
|
+
console.log(`(no semantic neighbors; basename '${basename}' too generic to fall back)`);
|
|
200
|
+
}
|
|
201
|
+
else if (mentions.length > 0) {
|
|
202
|
+
console.log(`(no semantic neighbors; showing ${mentions.length} files mentioning '${basename}')`);
|
|
203
|
+
for (const p of mentions) {
|
|
204
|
+
console.log(`imp: ${rel(p)}\t1`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
console.log("(none)");
|
|
209
|
+
}
|
|
153
210
|
}
|
|
154
211
|
}
|
|
155
212
|
else {
|
|
@@ -179,6 +236,21 @@ exports.related = new commander_1.Command("related")
|
|
|
179
236
|
else {
|
|
180
237
|
console.log("Dependents: none found");
|
|
181
238
|
}
|
|
239
|
+
if (topDeps.length === 0 && topRevs.length === 0) {
|
|
240
|
+
console.log("");
|
|
241
|
+
if (basenameRejected) {
|
|
242
|
+
console.log(`(basename '${basename}' too generic to fall back to mentions)`);
|
|
243
|
+
}
|
|
244
|
+
else if (mentions.length > 0) {
|
|
245
|
+
console.log(`Mentions of "${basename}" in other files:`);
|
|
246
|
+
for (const p of mentions) {
|
|
247
|
+
const rel = p.startsWith(`${projectRoot}/`)
|
|
248
|
+
? p.slice(projectRoot.length + 1)
|
|
249
|
+
: p;
|
|
250
|
+
console.log(` ${rel}`);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
182
254
|
}
|
|
183
255
|
}
|
|
184
256
|
catch (error) {
|
package/dist/commands/search.js
CHANGED
|
@@ -712,53 +712,26 @@ Examples:
|
|
|
712
712
|
process.exitCode = 1;
|
|
713
713
|
}
|
|
714
714
|
else {
|
|
715
|
-
//
|
|
716
|
-
|
|
715
|
+
// Pre-pass: group by absolute path in first-occurrence order so we
|
|
716
|
+
// can collapse same-file repeats under a one-line header. Score
|
|
717
|
+
// order is preserved globally for first-of-file; siblings cluster
|
|
718
|
+
// under their first hit's rank.
|
|
719
|
+
const groups = new Map();
|
|
717
720
|
for (const r of filteredData) {
|
|
718
721
|
const absP = (_j = (_g = r.path) !== null && _g !== void 0 ? _g : (_h = r.metadata) === null || _h === void 0 ? void 0 : _h.path) !== null && _j !== void 0 ? _j : "";
|
|
719
|
-
const
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
const startLine = Math.max(1, ((_o = (_l = (_k = r.startLine) !== null && _k !== void 0 ? _k : r.start_line) !== null && _l !== void 0 ? _l : (_m = r.generated_metadata) === null || _m === void 0 ? void 0 : _m.start_line) !== null && _o !== void 0 ? _o : 0) + 1);
|
|
723
|
-
const defs = Array.isArray(r.defined_symbols)
|
|
724
|
-
? r.defined_symbols
|
|
725
|
-
: [];
|
|
726
|
-
const symbol = defs[0] || "";
|
|
727
|
-
const role = ((_p = r.role) !== null && _p !== void 0 ? _p : "")
|
|
728
|
-
.slice(0, 4)
|
|
729
|
-
.toUpperCase();
|
|
730
|
-
let hint = "";
|
|
731
|
-
if (r.summary) {
|
|
732
|
-
hint = ` — ${r.summary}`;
|
|
722
|
+
const arr = groups.get(absP);
|
|
723
|
+
if (arr) {
|
|
724
|
+
arr.push(r);
|
|
733
725
|
}
|
|
734
726
|
else {
|
|
735
|
-
|
|
736
|
-
const raw = (_r = (_q = r.content) !== null && _q !== void 0 ? _q : r.text) !== null && _r !== void 0 ? _r : "";
|
|
737
|
-
const lines = raw.split("\n");
|
|
738
|
-
for (const line of lines) {
|
|
739
|
-
const trimmed = line.trim();
|
|
740
|
-
// Skip empty, comments, imports, braces, and mid-line fragments
|
|
741
|
-
if (!trimmed || trimmed.length < 5)
|
|
742
|
-
continue;
|
|
743
|
-
if (trimmed.startsWith("//") || trimmed.startsWith("/*") || trimmed.startsWith("*"))
|
|
744
|
-
continue;
|
|
745
|
-
if (trimmed.startsWith("import ") || trimmed.startsWith("#") || trimmed.startsWith("File:"))
|
|
746
|
-
continue;
|
|
747
|
-
if (trimmed === "{" || trimmed === "}")
|
|
748
|
-
continue;
|
|
749
|
-
// Skip lines that look like continuations (start with punctuation, closing braces, or spread)
|
|
750
|
-
if (/^[.),;:}\]|&(+`'"!~]/.test(trimmed))
|
|
751
|
-
continue;
|
|
752
|
-
if (trimmed.startsWith("} ") || trimmed.startsWith("- ") || trimmed.startsWith("..."))
|
|
753
|
-
continue;
|
|
754
|
-
// Skip lines that look like mid-expression fragments (no keyword/declaration prefix)
|
|
755
|
-
if (/^[a-z]/.test(trimmed) && !/^(export|function|class|interface|type|const|let|var|async|return|if|for|while|switch|enum|struct|pub |fn |def |impl |mod |use )/.test(trimmed))
|
|
756
|
-
continue;
|
|
757
|
-
hint = ` — ${trimmed.length > 120 ? trimmed.slice(0, 117) + "..." : trimmed}`;
|
|
758
|
-
break;
|
|
759
|
-
}
|
|
727
|
+
groups.set(absP, [r]);
|
|
760
728
|
}
|
|
761
|
-
|
|
729
|
+
}
|
|
730
|
+
const seenImportFiles = new Set();
|
|
731
|
+
for (const [absP, members] of groups) {
|
|
732
|
+
const relPath = absP.startsWith(effectiveRoot)
|
|
733
|
+
? absP.slice(effectiveRoot.length + 1)
|
|
734
|
+
: absP;
|
|
762
735
|
if (options.imports && absP && !seenImportFiles.has(absP)) {
|
|
763
736
|
seenImportFiles.add(absP);
|
|
764
737
|
const imports = getImportsForFile(absP);
|
|
@@ -766,14 +739,62 @@ Examples:
|
|
|
766
739
|
console.log(`[imports ${relPath}] ${imports.split("\n").join(" | ")}`);
|
|
767
740
|
}
|
|
768
741
|
}
|
|
769
|
-
const
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
const
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
742
|
+
const grouped = members.length > 1;
|
|
743
|
+
if (grouped) {
|
|
744
|
+
console.log(`${relPath} (${members.length} hits):`);
|
|
745
|
+
}
|
|
746
|
+
for (const r of members) {
|
|
747
|
+
const startLine = Math.max(1, ((_o = (_l = (_k = r.startLine) !== null && _k !== void 0 ? _k : r.start_line) !== null && _l !== void 0 ? _l : (_m = r.generated_metadata) === null || _m === void 0 ? void 0 : _m.start_line) !== null && _o !== void 0 ? _o : 0) + 1);
|
|
748
|
+
const defs = Array.isArray(r.defined_symbols)
|
|
749
|
+
? r.defined_symbols
|
|
750
|
+
: [];
|
|
751
|
+
const symbol = defs[0] || "";
|
|
752
|
+
const role = ((_p = r.role) !== null && _p !== void 0 ? _p : "")
|
|
753
|
+
.slice(0, 4)
|
|
754
|
+
.toUpperCase();
|
|
755
|
+
let hint = "";
|
|
756
|
+
if (r.summary) {
|
|
757
|
+
hint = ` — ${r.summary}`;
|
|
758
|
+
}
|
|
759
|
+
else {
|
|
760
|
+
// Extract first meaningful signature line from content
|
|
761
|
+
const raw = (_r = (_q = r.content) !== null && _q !== void 0 ? _q : r.text) !== null && _r !== void 0 ? _r : "";
|
|
762
|
+
const lines = raw.split("\n");
|
|
763
|
+
for (const line of lines) {
|
|
764
|
+
const trimmed = line.trim();
|
|
765
|
+
// Skip empty, comments, imports, braces, and mid-line fragments
|
|
766
|
+
if (!trimmed || trimmed.length < 5)
|
|
767
|
+
continue;
|
|
768
|
+
if (trimmed.startsWith("//") || trimmed.startsWith("/*") || trimmed.startsWith("*"))
|
|
769
|
+
continue;
|
|
770
|
+
if (trimmed.startsWith("import ") || trimmed.startsWith("#") || trimmed.startsWith("File:"))
|
|
771
|
+
continue;
|
|
772
|
+
if (trimmed === "{" || trimmed === "}")
|
|
773
|
+
continue;
|
|
774
|
+
// Skip lines that look like continuations (start with punctuation, closing braces, or spread)
|
|
775
|
+
if (/^[.),;:}\]|&(+`'"!~]/.test(trimmed))
|
|
776
|
+
continue;
|
|
777
|
+
if (trimmed.startsWith("} ") || trimmed.startsWith("- ") || trimmed.startsWith("..."))
|
|
778
|
+
continue;
|
|
779
|
+
// Skip lines that look like mid-expression fragments (no keyword/declaration prefix)
|
|
780
|
+
if (/^[a-z]/.test(trimmed) && !/^(export|function|class|interface|type|const|let|var|async|return|if|for|while|switch|enum|struct|pub |fn |def |impl |mod |use )/.test(trimmed))
|
|
781
|
+
continue;
|
|
782
|
+
hint = ` — ${trimmed.length > 120 ? trimmed.slice(0, 117) + "..." : trimmed}`;
|
|
783
|
+
break;
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
const sym = symbol ? ` ${symbol}` : "";
|
|
787
|
+
const rl = role ? ` [${role}]` : "";
|
|
788
|
+
const score = r.score;
|
|
789
|
+
const scoreCol = typeof score === "number" ? `\ts=${score.toFixed(3)}` : "";
|
|
790
|
+
const explainSuffix = options.explain && r.scoreBreakdown
|
|
791
|
+
? `\texplain:rerank=${r.scoreBreakdown.rerank.toFixed(3)},fused=${r.scoreBreakdown.fused.toFixed(3)},boost=${r.scoreBreakdown.boost.toFixed(2)}x,score=${r.scoreBreakdown.normalized.toFixed(3)}`
|
|
792
|
+
: "";
|
|
793
|
+
const locator = grouped
|
|
794
|
+
? ` :${startLine}`
|
|
795
|
+
: `${relPath}:${startLine}`;
|
|
796
|
+
console.log(`${locator}${scoreCol}${sym}${rl}${hint}${explainSuffix}`);
|
|
797
|
+
}
|
|
777
798
|
}
|
|
778
799
|
}
|
|
779
800
|
// Agent trace (compact)
|
|
@@ -93,14 +93,22 @@ exports.testFind = new commander_1.Command("test")
|
|
|
93
93
|
const rel = (p) => p.startsWith(`${projectRoot}/`) ? p.slice(projectRoot.length + 1) : p;
|
|
94
94
|
if (opts.agent) {
|
|
95
95
|
for (const t of tests) {
|
|
96
|
-
const hopLabel = t.hops ===
|
|
96
|
+
const hopLabel = t.hops === -1
|
|
97
|
+
? "via-import"
|
|
98
|
+
: t.hops === 0
|
|
99
|
+
? "direct"
|
|
100
|
+
: `${t.hops}-hop`;
|
|
97
101
|
console.log(`${rel(t.file)}:${t.line + 1}\t${t.symbol}\t${hopLabel}`);
|
|
98
102
|
}
|
|
99
103
|
}
|
|
100
104
|
else {
|
|
101
105
|
console.log(`Tests for ${target}:\n`);
|
|
102
106
|
for (const t of tests) {
|
|
103
|
-
const hopLabel = t.hops ===
|
|
107
|
+
const hopLabel = t.hops === -1
|
|
108
|
+
? "via import"
|
|
109
|
+
: t.hops === 0
|
|
110
|
+
? "calls directly"
|
|
111
|
+
: `${t.hops} hop${t.hops > 1 ? "s" : ""} away`;
|
|
104
112
|
console.log(` ${rel(t.file)}:${t.line + 1} ${t.symbol} (${hopLabel})`);
|
|
105
113
|
}
|
|
106
114
|
}
|
package/dist/commands/trace.js
CHANGED
|
@@ -43,6 +43,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
43
43
|
};
|
|
44
44
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
45
45
|
exports.trace = void 0;
|
|
46
|
+
const fs = __importStar(require("node:fs"));
|
|
46
47
|
const commander_1 = require("commander");
|
|
47
48
|
const graph_builder_1 = require("../lib/graph/graph-builder");
|
|
48
49
|
const formatter_1 = require("../lib/output/formatter");
|
|
@@ -50,6 +51,9 @@ const vector_db_1 = require("../lib/store/vector-db");
|
|
|
50
51
|
const exit_1 = require("../lib/utils/exit");
|
|
51
52
|
const project_registry_1 = require("../lib/utils/project-registry");
|
|
52
53
|
const project_root_1 = require("../lib/utils/project-root");
|
|
54
|
+
const useColors = process.stdout.isTTY && !process.env.NO_COLOR;
|
|
55
|
+
const dim = (s) => (useColors ? `\x1b[2m${s}\x1b[22m` : s);
|
|
56
|
+
const bold = (s) => (useColors ? `\x1b[1m${s}\x1b[22m` : s);
|
|
53
57
|
function formatTraceAgent(graph, projectRoot) {
|
|
54
58
|
if (!graph.center)
|
|
55
59
|
return "(not found)";
|
|
@@ -73,6 +77,117 @@ function formatTraceAgent(graph, projectRoot) {
|
|
|
73
77
|
}
|
|
74
78
|
return lines.join("\n");
|
|
75
79
|
}
|
|
80
|
+
function findCallSiteSnippet(fileCache, callerFile, callerLine, targetSymbol) {
|
|
81
|
+
if (!callerFile)
|
|
82
|
+
return null;
|
|
83
|
+
let lines = fileCache.get(callerFile);
|
|
84
|
+
if (!lines) {
|
|
85
|
+
try {
|
|
86
|
+
lines = fs.readFileSync(callerFile, "utf-8").split("\n");
|
|
87
|
+
}
|
|
88
|
+
catch (_a) {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
fileCache.set(callerFile, lines);
|
|
92
|
+
}
|
|
93
|
+
// Search a bounded window starting at the caller's definition line.
|
|
94
|
+
const start = Math.max(0, callerLine);
|
|
95
|
+
const end = Math.min(lines.length, callerLine + 200);
|
|
96
|
+
const wordRe = new RegExp(`\\b${targetSymbol.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`);
|
|
97
|
+
for (let i = start; i < end; i++) {
|
|
98
|
+
if (wordRe.test(lines[i])) {
|
|
99
|
+
return { snippet: lines[i].trim(), snippetLine: i };
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
// Window didn't contain the symbol — chunker rolled the reference up to a
|
|
103
|
+
// parent scope. Skip the snippet rather than showing a misleading default;
|
|
104
|
+
// the caller's file:line is still emitted.
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
function buildInboundTree(callerTree, targetSymbol, fileCache, withSnippets, limit) {
|
|
108
|
+
var _a, _b, _c;
|
|
109
|
+
const out = [];
|
|
110
|
+
// Dedupe by call-site location: when getCallers returns multiple chunks of
|
|
111
|
+
// the same file (e.g. several methods of a class) that all reference the
|
|
112
|
+
// target on the same line, collapse them into one row.
|
|
113
|
+
const seen = new Set();
|
|
114
|
+
for (const t of callerTree) {
|
|
115
|
+
const snippet = withSnippets
|
|
116
|
+
? findCallSiteSnippet(fileCache, t.node.file, t.node.line, targetSymbol)
|
|
117
|
+
: null;
|
|
118
|
+
const dedupeKey = `${t.node.file}:${(_a = snippet === null || snippet === void 0 ? void 0 : snippet.snippetLine) !== null && _a !== void 0 ? _a : t.node.line}`;
|
|
119
|
+
if (seen.has(dedupeKey))
|
|
120
|
+
continue;
|
|
121
|
+
seen.add(dedupeKey);
|
|
122
|
+
out.push({
|
|
123
|
+
symbol: t.node.symbol,
|
|
124
|
+
file: t.node.file,
|
|
125
|
+
line: t.node.line,
|
|
126
|
+
snippet: (_b = snippet === null || snippet === void 0 ? void 0 : snippet.snippet) !== null && _b !== void 0 ? _b : null,
|
|
127
|
+
snippetLine: (_c = snippet === null || snippet === void 0 ? void 0 : snippet.snippetLine) !== null && _c !== void 0 ? _c : null,
|
|
128
|
+
callers: buildInboundTree(t.callers, t.node.symbol, fileCache, withSnippets, limit),
|
|
129
|
+
});
|
|
130
|
+
if (out.length >= limit)
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
return out;
|
|
134
|
+
}
|
|
135
|
+
function formatInboundAgent(center, tree, projectRoot, withSnippets) {
|
|
136
|
+
const rel = (p) => p.startsWith(projectRoot) ? p.slice(projectRoot.length + 1) : p;
|
|
137
|
+
const lines = [];
|
|
138
|
+
lines.push(`${center.symbol}\t${rel(center.file)}:${center.line + 1}\t${center.role}`);
|
|
139
|
+
const walk = (nodes, depth) => {
|
|
140
|
+
var _a, _b;
|
|
141
|
+
for (const n of nodes) {
|
|
142
|
+
const prefix = " ".repeat(depth);
|
|
143
|
+
const loc = n.file ? `${rel(n.file)}:${((_a = n.snippetLine) !== null && _a !== void 0 ? _a : n.line) + 1}` : "(not indexed)";
|
|
144
|
+
const cols = withSnippets
|
|
145
|
+
? `${loc}\t${n.symbol}\t${(_b = n.snippet) !== null && _b !== void 0 ? _b : ""}`
|
|
146
|
+
: `${loc}\t${n.symbol}`;
|
|
147
|
+
lines.push(`${prefix}${cols}`);
|
|
148
|
+
walk(n.callers, depth + 1);
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
walk(tree, 0);
|
|
152
|
+
return lines.join("\n");
|
|
153
|
+
}
|
|
154
|
+
function formatInboundHuman(center, tree, projectRoot, withSnippets) {
|
|
155
|
+
const rel = (p) => p.startsWith(projectRoot) ? p.slice(projectRoot.length + 1) : p;
|
|
156
|
+
const flatCount = (() => {
|
|
157
|
+
let n = 0;
|
|
158
|
+
const walk = (nodes) => {
|
|
159
|
+
for (const node of nodes) {
|
|
160
|
+
n++;
|
|
161
|
+
walk(node.callers);
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
walk(tree);
|
|
165
|
+
return n;
|
|
166
|
+
})();
|
|
167
|
+
const lines = [];
|
|
168
|
+
lines.push(`${bold(`inbound callers of ${center.symbol}`)} ${dim(`(${flatCount})`)}`);
|
|
169
|
+
lines.push(` ${dim(`${rel(center.file)}:${center.line + 1} [${center.role}]`)}`);
|
|
170
|
+
if (tree.length === 0) {
|
|
171
|
+
lines.push(dim(" (none in scope)"));
|
|
172
|
+
return lines.join("\n");
|
|
173
|
+
}
|
|
174
|
+
const walk = (nodes, depth) => {
|
|
175
|
+
var _a;
|
|
176
|
+
for (const n of nodes) {
|
|
177
|
+
const indent = " ".repeat(depth + 1);
|
|
178
|
+
const loc = n.file
|
|
179
|
+
? `${rel(n.file)}:${((_a = n.snippetLine) !== null && _a !== void 0 ? _a : n.line) + 1}`
|
|
180
|
+
: "(not indexed)";
|
|
181
|
+
lines.push(`${indent}${n.symbol} ${dim(loc)}`);
|
|
182
|
+
if (withSnippets && n.snippet) {
|
|
183
|
+
lines.push(`${indent} ${dim(n.snippet)}`);
|
|
184
|
+
}
|
|
185
|
+
walk(n.callers, depth + 1);
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
walk(tree, 0);
|
|
189
|
+
return lines.join("\n");
|
|
190
|
+
}
|
|
76
191
|
exports.trace = new commander_1.Command("trace")
|
|
77
192
|
.description("Trace the call graph for a symbol")
|
|
78
193
|
.argument("<symbol>", "The symbol to trace")
|
|
@@ -81,9 +196,13 @@ exports.trace = new commander_1.Command("trace")
|
|
|
81
196
|
.option("--in <subpath>", "Restrict to a sub-path of the project (repeatable)", (value, prev) => (prev ? [...prev, value] : [value]))
|
|
82
197
|
.option("--exclude <subpath>", "Exclude a sub-path of the project (repeatable)", (value, prev) => (prev ? [...prev, value] : [value]))
|
|
83
198
|
.option("--agent", "Compact output for AI agents", false)
|
|
199
|
+
.option("--inbound", "Show only callers, with call-site snippets", false)
|
|
200
|
+
.option("--no-snippets", "Suppress call-site snippets in --inbound output")
|
|
201
|
+
.option("--limit <n>", "Max callers shown per node in --inbound (default 10)", "10")
|
|
84
202
|
.action((symbol, opts) => __awaiter(void 0, void 0, void 0, function* () {
|
|
85
203
|
var _a;
|
|
86
204
|
const depth = Math.min(Math.max(Number.parseInt(opts.depth || "1", 10), 1), 3);
|
|
205
|
+
const inboundLimit = Math.min(Math.max(Number.parseInt(opts.limit || "10", 10), 1), 30);
|
|
87
206
|
const root = (0, project_registry_1.resolveRootOrExit)(opts.root);
|
|
88
207
|
if (root === null)
|
|
89
208
|
return;
|
|
@@ -100,14 +219,32 @@ exports.trace = new commander_1.Command("trace")
|
|
|
100
219
|
});
|
|
101
220
|
const graphBuilder = new graph_builder_1.GraphBuilder(vectorDb, scope.pathPrefix, scope.excludePrefixes);
|
|
102
221
|
const graph = yield graphBuilder.buildGraphMultiHop(symbol, depth);
|
|
103
|
-
if (opts.
|
|
222
|
+
if (opts.inbound) {
|
|
223
|
+
if (!graph.center) {
|
|
224
|
+
console.log(opts.agent ? "(not found)" : `Symbol not found: ${symbol}`);
|
|
225
|
+
process.exitCode = 1;
|
|
226
|
+
}
|
|
227
|
+
else {
|
|
228
|
+
const fileCache = new Map();
|
|
229
|
+
const withSnippets = opts.snippets !== false;
|
|
230
|
+
const tree = buildInboundTree(graph.callerTree, symbol, fileCache, withSnippets, inboundLimit);
|
|
231
|
+
if (opts.agent) {
|
|
232
|
+
console.log(formatInboundAgent(graph.center, tree, projectRoot, withSnippets));
|
|
233
|
+
}
|
|
234
|
+
else {
|
|
235
|
+
console.log(formatInboundHuman(graph.center, tree, projectRoot, withSnippets));
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
else if (opts.agent) {
|
|
104
240
|
console.log(formatTraceAgent(graph, projectRoot));
|
|
241
|
+
if (!graph.center)
|
|
242
|
+
process.exitCode = 1;
|
|
105
243
|
}
|
|
106
244
|
else {
|
|
107
245
|
console.log((0, formatter_1.formatTrace)(graph, { symbol }));
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
process.exitCode = 1;
|
|
246
|
+
if (!graph.center)
|
|
247
|
+
process.exitCode = 1;
|
|
111
248
|
}
|
|
112
249
|
}
|
|
113
250
|
catch (error) {
|
package/dist/lib/graph/impact.js
CHANGED
|
@@ -95,6 +95,11 @@ function expandFileSymbols(symbols, vectorDb, projectRoot, excludePrefixes) {
|
|
|
95
95
|
}
|
|
96
96
|
/**
|
|
97
97
|
* Find test files that exercise a set of symbols, using reverse call graph traversal.
|
|
98
|
+
* When the call-graph walk returns nothing, falls back to test files that
|
|
99
|
+
* reference the symbol via referenced_symbols or textual content match —
|
|
100
|
+
* catching the common case where a test imports a symbol but doesn't call it
|
|
101
|
+
* through the graph the chunker captures (UI components, mocked modules, etc).
|
|
102
|
+
* Fallback hits are tagged with hops = -1.
|
|
98
103
|
*/
|
|
99
104
|
function findTests(symbols_1, vectorDb_1, projectRoot_1) {
|
|
100
105
|
return __awaiter(this, arguments, void 0, function* (symbols, vectorDb, projectRoot, depth = 1, excludePrefixes) {
|
|
@@ -105,9 +110,55 @@ function findTests(symbols_1, vectorDb_1, projectRoot_1) {
|
|
|
105
110
|
for (const symbol of expanded) {
|
|
106
111
|
yield walkCallers(symbol, graphBuilder, testHits, 0, depth, new Set());
|
|
107
112
|
}
|
|
113
|
+
if (testHits.size === 0) {
|
|
114
|
+
const importFiles = yield findImportFallbackTests(expanded, vectorDb, projectRoot, excludePrefixes);
|
|
115
|
+
for (const file of importFiles) {
|
|
116
|
+
testHits.set(`${file}:(referenced)`, {
|
|
117
|
+
file,
|
|
118
|
+
symbol: "(referenced)",
|
|
119
|
+
line: 0,
|
|
120
|
+
hops: -1,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
}
|
|
108
124
|
return [...testHits.values()].sort((a, b) => a.hops - b.hops || a.file.localeCompare(b.file));
|
|
109
125
|
});
|
|
110
126
|
}
|
|
127
|
+
function findImportFallbackTests(symbols, vectorDb, projectRoot, excludePrefixes) {
|
|
128
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
129
|
+
const files = new Set();
|
|
130
|
+
// 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);
|
|
133
|
+
for (const d of dependents) {
|
|
134
|
+
if (isTestPath(d.file))
|
|
135
|
+
files.add(d.file);
|
|
136
|
+
}
|
|
137
|
+
// Signal 2: content LIKE match (textual; survives chunker quirks where a
|
|
138
|
+
// test body's referenced_symbols ends up empty).
|
|
139
|
+
const table = yield vectorDb.ensureTable();
|
|
140
|
+
const prefix = projectRoot.endsWith("/") ? projectRoot : `${projectRoot}/`;
|
|
141
|
+
let pathScope = `path LIKE '${(0, filter_builder_1.escapeSqlString)(prefix)}%'`;
|
|
142
|
+
for (const ex of excludePrefixes !== null && excludePrefixes !== void 0 ? excludePrefixes : []) {
|
|
143
|
+
const exNorm = ex.endsWith("/") ? ex : `${ex}/`;
|
|
144
|
+
pathScope += ` AND path NOT LIKE '${(0, filter_builder_1.escapeSqlString)(exNorm)}%'`;
|
|
145
|
+
}
|
|
146
|
+
for (const sym of symbols) {
|
|
147
|
+
const rows = yield table
|
|
148
|
+
.query()
|
|
149
|
+
.select(["path"])
|
|
150
|
+
.where(`content LIKE '%${(0, filter_builder_1.escapeSqlString)(sym)}%' AND ${pathScope}`)
|
|
151
|
+
.limit(100)
|
|
152
|
+
.toArray();
|
|
153
|
+
for (const row of rows) {
|
|
154
|
+
const p = String(row.path || "");
|
|
155
|
+
if (isTestPath(p))
|
|
156
|
+
files.add(p);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return files;
|
|
160
|
+
});
|
|
161
|
+
}
|
|
111
162
|
function walkCallers(symbol, graphBuilder, testHits, currentHop, maxDepth, visited) {
|
|
112
163
|
return __awaiter(this, void 0, void 0, function* () {
|
|
113
164
|
if (visited.has(symbol))
|
|
@@ -0,0 +1,78 @@
|
|
|
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.fetchTestsForFooter = fetchTestsForFooter;
|
|
13
|
+
exports.renderTestsFooterAgent = renderTestsFooterAgent;
|
|
14
|
+
exports.renderTestsFooterHuman = renderTestsFooterHuman;
|
|
15
|
+
const impact_1 = require("../graph/impact");
|
|
16
|
+
const FOOTER_TIMEOUT_MS = 1500;
|
|
17
|
+
const MAX_SHOWN = 5;
|
|
18
|
+
const useColors = process.stdout.isTTY && !process.env.NO_COLOR;
|
|
19
|
+
const dim = (s) => (useColors ? `\x1b[2m${s}\x1b[22m` : s);
|
|
20
|
+
function withTimeout(p, ms) {
|
|
21
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
22
|
+
let timer;
|
|
23
|
+
const timeout = new Promise((resolve) => {
|
|
24
|
+
timer = setTimeout(() => resolve(null), ms);
|
|
25
|
+
});
|
|
26
|
+
try {
|
|
27
|
+
return yield Promise.race([p, timeout]);
|
|
28
|
+
}
|
|
29
|
+
finally {
|
|
30
|
+
if (timer)
|
|
31
|
+
clearTimeout(timer);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
function fetchTestsForFooter(symbol, vectorDb, pathPrefix, excludePrefixes) {
|
|
36
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
37
|
+
return withTimeout((0, impact_1.findTests)([symbol], vectorDb, pathPrefix, 1, excludePrefixes), FOOTER_TIMEOUT_MS);
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
function relPath(p, projectRoot) {
|
|
41
|
+
return p.startsWith(`${projectRoot}/`) ? p.slice(projectRoot.length + 1) : p;
|
|
42
|
+
}
|
|
43
|
+
function hopLabelAgent(hops) {
|
|
44
|
+
if (hops === -1)
|
|
45
|
+
return "via-import";
|
|
46
|
+
if (hops === 0)
|
|
47
|
+
return "direct";
|
|
48
|
+
return `${hops}-hop`;
|
|
49
|
+
}
|
|
50
|
+
function hopLabelHuman(hops) {
|
|
51
|
+
if (hops === -1)
|
|
52
|
+
return "via import";
|
|
53
|
+
if (hops === 0)
|
|
54
|
+
return "direct";
|
|
55
|
+
return `${hops} hop${hops > 1 ? "s" : ""}`;
|
|
56
|
+
}
|
|
57
|
+
function renderTestsFooterAgent(tests, projectRoot) {
|
|
58
|
+
const lines = [];
|
|
59
|
+
for (const t of tests.slice(0, MAX_SHOWN)) {
|
|
60
|
+
lines.push(`t: ${relPath(t.file, projectRoot)}:${t.line + 1}\t${t.symbol}\t${hopLabelAgent(t.hops)}`);
|
|
61
|
+
}
|
|
62
|
+
if (tests.length > MAX_SHOWN) {
|
|
63
|
+
lines.push(`t: ... ${tests.length - MAX_SHOWN} more`);
|
|
64
|
+
}
|
|
65
|
+
return lines;
|
|
66
|
+
}
|
|
67
|
+
function renderTestsFooterHuman(tests, projectRoot) {
|
|
68
|
+
const lines = [];
|
|
69
|
+
lines.push("");
|
|
70
|
+
lines.push(`tests (${tests.length}):`);
|
|
71
|
+
for (const t of tests.slice(0, MAX_SHOWN)) {
|
|
72
|
+
lines.push(` ${t.symbol.padEnd(25)} ${dim(`${relPath(t.file, projectRoot)}:${t.line + 1}`)} ${dim(`(${hopLabelHuman(t.hops)})`)}`);
|
|
73
|
+
}
|
|
74
|
+
if (tests.length > MAX_SHOWN) {
|
|
75
|
+
lines.push(dim(` ... and ${tests.length - MAX_SHOWN} more`));
|
|
76
|
+
}
|
|
77
|
+
return lines;
|
|
78
|
+
}
|
package/package.json
CHANGED
|
@@ -175,8 +175,28 @@ async function main() {
|
|
|
175
175
|
const response = {
|
|
176
176
|
hookSpecificOutput: {
|
|
177
177
|
hookEventName: "SessionStart",
|
|
178
|
-
additionalContext:
|
|
179
|
-
|
|
178
|
+
additionalContext: `gmax ready. Add --agent to any command for compact output (~89% fewer tokens).
|
|
179
|
+
|
|
180
|
+
Find:
|
|
181
|
+
gmax "topic" semantic search
|
|
182
|
+
gmax similar <symbol> similar code
|
|
183
|
+
|
|
184
|
+
Understand:
|
|
185
|
+
gmax peek <symbol> signature + callers + callees + tests
|
|
186
|
+
gmax extract <symbol> full body + tests
|
|
187
|
+
gmax trace <symbol> call graph (--inbound = callers + snippets)
|
|
188
|
+
gmax test <symbol> tests for symbol
|
|
189
|
+
gmax impact <symbol> blast radius
|
|
190
|
+
|
|
191
|
+
Survey:
|
|
192
|
+
gmax skeleton <file> file structure (file path, NOT a directory)
|
|
193
|
+
gmax context "topic" --budget 4000 multi-file topic summary
|
|
194
|
+
gmax log <path-or-symbol> git commits (replaces recent/diff)
|
|
195
|
+
gmax status indexed projects
|
|
196
|
+
|
|
197
|
+
Scope flags: --root <name|path>, --in <subpath>, --exclude <subpath>.
|
|
198
|
+
Roles in results: [DEFI] [ORCH] [IMPL] [DOCS].
|
|
199
|
+
Recovery: "not added yet" → gmax add; stale results → gmax index.`,
|
|
180
200
|
},
|
|
181
201
|
};
|
|
182
202
|
process.stdout.write(JSON.stringify(response));
|
|
@@ -52,7 +52,7 @@ async function main() {
|
|
|
52
52
|
hookSpecificOutput: {
|
|
53
53
|
hookEventName: "SubagentStart",
|
|
54
54
|
additionalContext:
|
|
55
|
-
'gmax
|
|
55
|
+
'gmax available: gmax "query" --agent (search), gmax peek/extract/trace <symbol> --agent (symbol info / call graph). Stale results → gmax index.',
|
|
56
56
|
},
|
|
57
57
|
};
|
|
58
58
|
process.stdout.write(JSON.stringify(response));
|
|
@@ -53,7 +53,7 @@ gmax "auth" --root ~/other/project --agent # search a different project
|
|
|
53
53
|
gmax "auth" --imports --agent # show file imports per file
|
|
54
54
|
```
|
|
55
55
|
|
|
56
|
-
Output: `file:line symbol [ROLE] — signature_hint` (one line per result)
|
|
56
|
+
Output: `file:line symbol [ROLE] — signature_hint` (one line per result). When multiple hits land in the same file, they collapse under a `path/to/file.ts (N hits):` header with indented children — saves the repeated path prefix.
|
|
57
57
|
|
|
58
58
|
All search flags: `--agent --plain -m <n> --per-file <n> --min-score <n> --root <dir> --file <name> --exclude <prefix> --lang <ext> --role <role> --symbol --imports --name <regex> -C <n> --compact --content --scores --skeleton --explain --context-for-llm --budget <tokens>`
|
|
59
59
|
|
|
@@ -87,6 +87,9 @@ gmax trace handleAuth # 1-hop: callers + callees
|
|
|
87
87
|
gmax trace handleAuth -d 2 # 2-hop: callers-of-callers
|
|
88
88
|
gmax trace handleAuth --root ~/project # trace in a different project
|
|
89
89
|
gmax trace handleAuth --agent # compact: symbol\tpath:line, <- callers, -> callees
|
|
90
|
+
gmax trace handleAuth --inbound --agent # callers-only with call-site snippets (path:line\tsymbol\tsnippet)
|
|
91
|
+
gmax trace handleAuth --inbound --no-snippets # drop the snippet column
|
|
92
|
+
gmax trace handleAuth --inbound --limit 20 # show up to 20 callers (default 10, max 30)
|
|
90
93
|
```
|
|
91
94
|
|
|
92
95
|
### Extract — `gmax extract <symbol>`
|
|
@@ -95,7 +98,9 @@ gmax extract handleAuth # full function body with line number
|
|
|
95
98
|
gmax extract handleAuth --agent # compact: path:start-end then raw code
|
|
96
99
|
gmax extract handleAuth --imports # prepend file imports
|
|
97
100
|
gmax extract handleAuth --root ~/project # extract from different project
|
|
101
|
+
gmax extract handleAuth --no-tests # suppress tests footer (default-on)
|
|
98
102
|
```
|
|
103
|
+
Output ends with a `--- tests:` footer listing tests that exercise the symbol (default-on; opt-out with `--no-tests`).
|
|
99
104
|
|
|
100
105
|
### Peek — `gmax peek <symbol>`
|
|
101
106
|
```
|
|
@@ -103,7 +108,9 @@ gmax peek handleAuth # signature + callers + callees
|
|
|
103
108
|
gmax peek handleAuth --agent # compact TSV output
|
|
104
109
|
gmax peek handleAuth -d 2 # 2-hop callers
|
|
105
110
|
gmax peek handleAuth --root ~/project # peek in different project
|
|
111
|
+
gmax peek handleAuth --no-tests # suppress tests footer (default-on)
|
|
106
112
|
```
|
|
113
|
+
Agent output ends with `t: <test-file>:line\t<test-symbol>\t<hop-label>` rows where hop-label is `direct`, `N-hop`, or `via-import`.
|
|
107
114
|
|
|
108
115
|
### Skeleton — `gmax skeleton <target>`
|
|
109
116
|
```
|
|
@@ -124,6 +131,7 @@ gmax project --root ~/other/project # different project
|
|
|
124
131
|
gmax related src/lib/index/syncer.ts # dependencies + dependents
|
|
125
132
|
gmax related src/lib/index/syncer.ts --root ~/project
|
|
126
133
|
```
|
|
134
|
+
When both directions are empty, falls back to files mentioning the input file's basename. Generic basenames (`index`, `main`, `lib`, etc.) skip the fallback.
|
|
127
135
|
|
|
128
136
|
### Commit history — `gmax log <path-or-symbol>`
|
|
129
137
|
```
|
|
@@ -148,11 +156,12 @@ gmax symbols --agent # compact: symbol\tpath:line\tcount
|
|
|
148
156
|
|
|
149
157
|
### Test — `gmax test <symbol|file>`
|
|
150
158
|
```
|
|
151
|
-
gmax test handleAuth # tests calling handleAuth
|
|
159
|
+
gmax test handleAuth # tests calling handleAuth (call-graph + import-fallback)
|
|
152
160
|
gmax test src/lib/auth.ts # tests for symbols in this file
|
|
153
161
|
gmax test handleAuth -d 2 # 2-hop: tests calling callers too
|
|
154
162
|
gmax test handleAuth --agent # compact output
|
|
155
163
|
```
|
|
164
|
+
Hop labels: `direct` (calls the symbol), `N-hop` (calls a transitive caller), `via-import` (test references the symbol via import or mock when no call-graph match exists).
|
|
156
165
|
|
|
157
166
|
### Impact — `gmax impact <symbol|file>`
|
|
158
167
|
```
|