grepmax 0.16.2 → 0.16.4
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/diff.js +8 -140
- package/dist/commands/extract.js +20 -0
- package/dist/commands/log.js +231 -0
- package/dist/commands/peek.js +19 -0
- package/dist/commands/recent.js +7 -129
- 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/index.js +2 -0
- package/dist/lib/graph/impact.js +51 -0
- package/dist/lib/utils/git.js +90 -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/skills/grepmax/SKILL.md +22 -13
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/index.js
CHANGED
|
@@ -52,6 +52,7 @@ const index_1 = require("./commands/index");
|
|
|
52
52
|
const investigate_1 = require("./commands/investigate");
|
|
53
53
|
const list_1 = require("./commands/list");
|
|
54
54
|
const llm_1 = require("./commands/llm");
|
|
55
|
+
const log_1 = require("./commands/log");
|
|
55
56
|
const mcp_1 = require("./commands/mcp");
|
|
56
57
|
const peek_1 = require("./commands/peek");
|
|
57
58
|
const project_1 = require("./commands/project");
|
|
@@ -115,6 +116,7 @@ commander_1.program.addCommand(extract_1.extract);
|
|
|
115
116
|
commander_1.program.addCommand(peek_1.peek);
|
|
116
117
|
commander_1.program.addCommand(project_1.project);
|
|
117
118
|
commander_1.program.addCommand(related_1.related);
|
|
119
|
+
commander_1.program.addCommand(log_1.log);
|
|
118
120
|
commander_1.program.addCommand(recent_1.recent);
|
|
119
121
|
commander_1.program.addCommand(diff_1.diff);
|
|
120
122
|
commander_1.program.addCommand(test_find_1.testFind);
|
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))
|
package/dist/lib/utils/git.js
CHANGED
|
@@ -37,6 +37,7 @@ exports.isWorktree = isWorktree;
|
|
|
37
37
|
exports.getGitCommonDir = getGitCommonDir;
|
|
38
38
|
exports.getMainRepoRoot = getMainRepoRoot;
|
|
39
39
|
exports.getChangedFiles = getChangedFiles;
|
|
40
|
+
exports.getCommitHistory = getCommitHistory;
|
|
40
41
|
exports.getUntrackedFiles = getUntrackedFiles;
|
|
41
42
|
const node_child_process_1 = require("node:child_process");
|
|
42
43
|
const fs = __importStar(require("node:fs"));
|
|
@@ -115,6 +116,95 @@ function getChangedFiles(ref, cwd) {
|
|
|
115
116
|
return [];
|
|
116
117
|
}
|
|
117
118
|
}
|
|
119
|
+
/**
|
|
120
|
+
* Get commit history for one or more paths. When paths.length > 1 (symbol
|
|
121
|
+
* fan-out), git natively dedupes commits across paths. --follow only works
|
|
122
|
+
* with a single path; auto-disabled otherwise.
|
|
123
|
+
*/
|
|
124
|
+
function getCommitHistory(opts) {
|
|
125
|
+
var _a;
|
|
126
|
+
if (opts.paths.length === 0)
|
|
127
|
+
return [];
|
|
128
|
+
const args = [
|
|
129
|
+
"log",
|
|
130
|
+
"--pretty=format:%x1e%H%x1f%aN%x1f%aI%x1f%ar%x1f%s",
|
|
131
|
+
"--numstat",
|
|
132
|
+
];
|
|
133
|
+
if (opts.follow && opts.paths.length === 1)
|
|
134
|
+
args.push("--follow");
|
|
135
|
+
if (opts.limit > 0)
|
|
136
|
+
args.push(`-n${opts.limit}`);
|
|
137
|
+
if (opts.since)
|
|
138
|
+
args.push(`--since=${opts.since}`);
|
|
139
|
+
if (opts.author)
|
|
140
|
+
args.push(`--author=${opts.author}`);
|
|
141
|
+
if (opts.from)
|
|
142
|
+
args.push(`${opts.from}..HEAD`);
|
|
143
|
+
args.push("--");
|
|
144
|
+
for (const p of opts.paths)
|
|
145
|
+
args.push(p);
|
|
146
|
+
const execOpts = {
|
|
147
|
+
cwd: (_a = opts.cwd) !== null && _a !== void 0 ? _a : process.cwd(),
|
|
148
|
+
encoding: "utf-8",
|
|
149
|
+
timeout: 10000,
|
|
150
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
151
|
+
};
|
|
152
|
+
let output;
|
|
153
|
+
try {
|
|
154
|
+
output = (0, node_child_process_1.execFileSync)("git", args, execOpts);
|
|
155
|
+
}
|
|
156
|
+
catch (_b) {
|
|
157
|
+
return [];
|
|
158
|
+
}
|
|
159
|
+
const records = output.split("\x1e").filter((r) => r.length > 0);
|
|
160
|
+
const commits = [];
|
|
161
|
+
for (const record of records) {
|
|
162
|
+
const lines = record.split("\n");
|
|
163
|
+
if (lines.length === 0)
|
|
164
|
+
continue;
|
|
165
|
+
const headerFields = lines[0].split("\x1f");
|
|
166
|
+
if (headerFields.length < 5)
|
|
167
|
+
continue;
|
|
168
|
+
const [hash, author, isoDate, relDate, subject] = headerFields;
|
|
169
|
+
const numstatLines = [];
|
|
170
|
+
let insertions = 0;
|
|
171
|
+
let deletions = 0;
|
|
172
|
+
for (let i = 1; i < lines.length; i++) {
|
|
173
|
+
const line = lines[i].trim();
|
|
174
|
+
if (!line)
|
|
175
|
+
continue;
|
|
176
|
+
const parts = line.split("\t");
|
|
177
|
+
if (parts.length < 3)
|
|
178
|
+
continue;
|
|
179
|
+
// Binary diffs report '-' for added/removed; treat as 0.
|
|
180
|
+
const added = parts[0] === "-" ? 0 : Number.parseInt(parts[0], 10);
|
|
181
|
+
const removed = parts[1] === "-" ? 0 : Number.parseInt(parts[1], 10);
|
|
182
|
+
const path = parts.slice(2).join("\t");
|
|
183
|
+
if (Number.isFinite(added))
|
|
184
|
+
insertions += added;
|
|
185
|
+
if (Number.isFinite(removed))
|
|
186
|
+
deletions += removed;
|
|
187
|
+
numstatLines.push({
|
|
188
|
+
added: Number.isFinite(added) ? added : 0,
|
|
189
|
+
removed: Number.isFinite(removed) ? removed : 0,
|
|
190
|
+
path,
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
commits.push({
|
|
194
|
+
hash,
|
|
195
|
+
shortHash: hash.slice(0, 7),
|
|
196
|
+
author,
|
|
197
|
+
isoDate,
|
|
198
|
+
relDate,
|
|
199
|
+
subject,
|
|
200
|
+
filesChanged: numstatLines.length,
|
|
201
|
+
insertions,
|
|
202
|
+
deletions,
|
|
203
|
+
numstatLines,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
return commits;
|
|
207
|
+
}
|
|
118
208
|
/**
|
|
119
209
|
* Get untracked files (not yet added to git).
|
|
120
210
|
* Returns absolute paths.
|