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.
@@ -47,6 +47,7 @@ const fs = __importStar(require("node:fs"));
47
47
  const os = __importStar(require("node:os"));
48
48
  const path = __importStar(require("node:path"));
49
49
  const commander_1 = require("commander");
50
+ const config_1 = require("../config");
50
51
  const grammar_loader_1 = require("../lib/index/grammar-loader");
51
52
  const index_config_1 = require("../lib/index/index-config");
52
53
  const sync_helpers_1 = require("../lib/index/sync-helpers");
@@ -218,7 +219,7 @@ Examples:
218
219
  if (!done.ok) {
219
220
  throw new Error((_c = done.error) !== null && _c !== void 0 ? _c : "daemon add failed");
220
221
  }
221
- (0, project_registry_1.registerProject)(Object.assign(Object.assign({}, pendingEntry), { lastIndexed: new Date().toISOString(), chunkCount: (_d = done.indexed) !== null && _d !== void 0 ? _d : 0, status: "indexed" }));
222
+ (0, project_registry_1.registerProject)(Object.assign(Object.assign({}, pendingEntry), { lastIndexed: new Date().toISOString(), chunkCount: (_d = done.indexed) !== null && _d !== void 0 ? _d : 0, status: "indexed", chunkerVersion: config_1.CONFIG.CHUNKER_VERSION }));
222
223
  const failedFiles = (_e = done.failedFiles) !== null && _e !== void 0 ? _e : 0;
223
224
  const failedSuffix = failedFiles > 0 ? ` · ${failedFiles} failed` : "";
224
225
  spinner.succeed(`Added ${projectName} (${done.total} files, ${done.indexed} chunks${failedSuffix})`);
@@ -239,7 +240,7 @@ Examples:
239
240
  projectRoot,
240
241
  onProgress,
241
242
  });
242
- (0, project_registry_1.registerProject)(Object.assign(Object.assign({}, pendingEntry), { lastIndexed: new Date().toISOString(), chunkCount: result.indexed, status: "indexed" }));
243
+ (0, project_registry_1.registerProject)(Object.assign(Object.assign({}, pendingEntry), { lastIndexed: new Date().toISOString(), chunkCount: result.indexed, status: "indexed", chunkerVersion: config_1.CONFIG.CHUNKER_VERSION }));
243
244
  const failedSuffix = result.failedFiles > 0 ? ` · ${result.failedFiles} failed` : "";
244
245
  spinner.succeed(`Added ${projectName} (${result.total} files, ${result.indexed} chunks${failedSuffix})`);
245
246
  }
@@ -45,6 +45,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
45
45
  exports.audit = void 0;
46
46
  exports.computeAudit = computeAudit;
47
47
  const commander_1 = require("commander");
48
+ const callsites_1 = require("../lib/graph/callsites");
48
49
  const arrow_1 = require("../lib/utils/arrow");
49
50
  const exit_1 = require("../lib/utils/exit");
50
51
  const project_registry_1 = require("../lib/utils/project-registry");
@@ -71,8 +72,12 @@ function rel(p, prefix) {
71
72
  * `top` caps each list; `deadTotal` reports the full pre-cap dead count.
72
73
  */
73
74
  function computeAudit(rows, prefix, top) {
75
+ var _a, _b;
74
76
  // First definition of a symbol wins (matches GraphBuilder semantics).
75
77
  const defs = new Map();
78
+ // Distinct files defining each name — name-based edges can't tell same-name
79
+ // symbols apart, so multi-file definitions get flagged in the output.
80
+ const defFileCounts = new Map();
76
81
  // Distinct files that reference a symbol (cross-file inbound edges).
77
82
  const inboundFiles = new Map();
78
83
  const inboundTotal = new Map();
@@ -91,6 +96,9 @@ function computeAudit(rows, prefix, top) {
91
96
  if (!defs.has(s)) {
92
97
  defs.set(s, { file, line, exported, complexity: 0 });
93
98
  }
99
+ if (!defFileCounts.has(s))
100
+ defFileCounts.set(s, new Set());
101
+ defFileCounts.get(s).add(file);
94
102
  if (!fileDefs.has(file))
95
103
  fileDefs.set(file, new Set());
96
104
  fileDefs.get(file).add(s);
@@ -110,6 +118,10 @@ function computeAudit(rows, prefix, top) {
110
118
  for (const [symbol, info] of defs) {
111
119
  if (symbol.length < MIN_GOD_NAME_LEN)
112
120
  continue;
121
+ // Builtin method names (get, set, push, …) leak in via prototype/member
122
+ // definitions and their inbound counts are meaningless name collisions.
123
+ if ((0, callsites_1.isBuiltinCallee)(symbol))
124
+ continue;
113
125
  const refFiles = inboundFiles.get(symbol);
114
126
  if (!refFiles)
115
127
  continue;
@@ -125,6 +137,7 @@ function computeAudit(rows, prefix, top) {
125
137
  line: info.line,
126
138
  inboundFiles: external,
127
139
  totalRefs: inboundTotal.get(symbol) || 0,
140
+ defFiles: (_b = (_a = defFileCounts.get(symbol)) === null || _a === void 0 ? void 0 : _a.size) !== null && _b !== void 0 ? _b : 1,
128
141
  });
129
142
  }
130
143
  godNodes.sort((a, b) => b.inboundFiles - a.inboundFiles || b.totalRefs - a.totalRefs);
@@ -187,7 +200,10 @@ function formatHuman(r) {
187
200
  }
188
201
  else {
189
202
  for (const g of r.godNodes) {
190
- out.push(` ${style.cyan(g.symbol.padEnd(28))} ${style.dim(`${g.inboundFiles} files`)}, ${g.totalRefs} refs ${style.dim(`${g.file}:${g.line + 1}`)}`);
203
+ const ambiguous = g.defFiles > 1
204
+ ? style.dim(` ~defined in ${g.defFiles} files, location is a guess`)
205
+ : "";
206
+ out.push(` ${style.cyan(g.symbol.padEnd(28))} ${style.dim(`${g.inboundFiles} files`)}, ${g.totalRefs} refs ${style.dim(`${g.file}:${g.line + 1}`)}${ambiguous}`);
191
207
  }
192
208
  }
193
209
  out.push("");
@@ -224,7 +240,8 @@ function formatAgent(r) {
224
240
  const lines = [];
225
241
  lines.push(`scanned\t${r.scannedChunks}\t${r.scannedFiles}`);
226
242
  for (const g of r.godNodes) {
227
- lines.push(`god\t${g.symbol}\t${g.file}:${g.line + 1}\t${g.inboundFiles}\t${g.totalRefs}`);
243
+ const ambiguous = g.defFiles > 1 ? `\tdefs=${g.defFiles}` : "";
244
+ lines.push(`god\t${g.symbol}\t${g.file}:${g.line + 1}\t${g.inboundFiles}\t${g.totalRefs}${ambiguous}`);
228
245
  }
229
246
  for (const h of r.hubFiles) {
230
247
  lines.push(`hub\t${h.file}\t${h.dependents}\t${h.defines}\t${h.fanOut}`);
@@ -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.context = void 0;
46
+ exports.findEnclosingSignature = findEnclosingSignature;
46
47
  const fs = __importStar(require("node:fs"));
47
48
  const path = __importStar(require("node:path"));
48
49
  const commander_1 = require("commander");
@@ -82,6 +83,23 @@ function chunkEndLine(chunk) {
82
83
  const start = chunkStartLine(chunk);
83
84
  return Number((_d = (_b = (_a = chunk.end_line) !== null && _a !== void 0 ? _a : chunk.endLine) !== null && _b !== void 0 ? _b : (_c = chunk.generated_metadata) === null || _c === void 0 ? void 0 : _c.end_line) !== null && _d !== void 0 ? _d : start);
84
85
  }
86
+ /**
87
+ * The definition line of `parentSymbol` nearest above `startLine` — used to
88
+ * give a mid-function sub-chunk extract its enclosing signature. Requires a
89
+ * definition-shaped line, not just any mention, so recursive calls or
90
+ * references between the definition and the chunk don't win.
91
+ */
92
+ function findEnclosingSignature(lines, startLine, parentSymbol) {
93
+ if (!parentSymbol || !/^\w+$/.test(parentSymbol))
94
+ return null;
95
+ const defRe = new RegExp(`(?:\\b(?:class|function|interface|enum|struct|trait|impl|def|fn|type|const|let|var)\\b[^=]*\\b${parentSymbol}\\b|\\b${parentSymbol}\\b\\s*[(:=])`);
96
+ for (let i = Math.min(startLine, lines.length) - 1; i >= 0; i--) {
97
+ if (defRe.test(lines[i])) {
98
+ return { text: lines[i].trim(), line: i };
99
+ }
100
+ }
101
+ return null;
102
+ }
85
103
  function resolveExistingPath(target, root, projectRoot) {
86
104
  const candidates = [
87
105
  path.isAbsolute(target) ? target : path.resolve(root, target),
@@ -196,7 +214,8 @@ exports.context = new commander_1.Command("context")
196
214
  for (const r of entryPoints.slice(0, 5)) {
197
215
  const p = chunkPath(r);
198
216
  const line = chunkStartLine(r);
199
- const sym = (_c = (_b = (0, arrow_1.toArr)(r.defined_symbols)) === null || _b === void 0 ? void 0 : _b[0]) !== null && _c !== void 0 ? _c : "";
217
+ const parentSym = String(r.parent_symbol || "");
218
+ const sym = (_c = (_b = (0, arrow_1.toArr)(r.defined_symbols)) === null || _b === void 0 ? void 0 : _b[0]) !== null && _c !== void 0 ? _c : (parentSym ? `(in ${parentSym})` : "");
200
219
  const role = String(r.role || "IMPLEMENTATION");
201
220
  epSection.push(`${relPath(projectRoot, p)}:${line + 1} ${sym} [${role}]`);
202
221
  }
@@ -215,13 +234,26 @@ exports.context = new commander_1.Command("context")
215
234
  const startLine = chunkStartLine(r);
216
235
  const endLine = chunkEndLine(r);
217
236
  const sym = (_b = (_a = (0, arrow_1.toArr)(r.defined_symbols)) === null || _a === void 0 ? void 0 : _a[0]) !== null && _b !== void 0 ? _b : "";
237
+ const parentSym = String(r.parent_symbol || "");
218
238
  try {
219
239
  const content = fs.readFileSync(absP, "utf-8");
220
240
  const allLines = content.split("\n");
221
241
  const body = allLines
222
242
  .slice(startLine, Math.min(endLine + 1, allLines.length))
223
243
  .join("\n");
224
- return `\n--- ${relPath(projectRoot, absP)}:${startLine + 1} ${sym} ---\n${body}`;
244
+ // A sub-chunk that starts mid-function has no defined symbol of its
245
+ // own — prepend the enclosing definition's signature so the extract
246
+ // isn't headless code.
247
+ let enclosing = "";
248
+ let label = sym;
249
+ if (!sym && parentSym) {
250
+ label = `(in ${parentSym})`;
251
+ const sig = findEnclosingSignature(allLines, startLine, parentSym);
252
+ if (sig) {
253
+ enclosing = `${sig.text} // :${sig.line + 1}\n// ...\n`;
254
+ }
255
+ }
256
+ return `\n--- ${relPath(projectRoot, absP)}:${startLine + 1} ${label} ---\n${enclosing}${body}`;
225
257
  }
226
258
  catch (_c) {
227
259
  return null; // File not readable — drop
@@ -237,6 +237,11 @@ exports.doctor = new commander_1.Command("doctor")
237
237
  diskLevel = availBytes < config_1.DISK_CRITICAL_BYTES ? "CRITICAL" : availBytes < config_1.DISK_LOW_BYTES ? "LOW" : "ok";
238
238
  }
239
239
  catch (_e) { }
240
+ const staleChunkerProjects = projects.filter((p) => {
241
+ var _a;
242
+ return p.status === "indexed" &&
243
+ ((_a = p.chunkerVersion) !== null && _a !== void 0 ? _a : 1) < config_1.CONFIG.CHUNKER_VERSION;
244
+ });
240
245
  if (opts.agent) {
241
246
  const fields = [
242
247
  "index_health",
@@ -251,8 +256,14 @@ exports.doctor = new commander_1.Command("doctor")
251
256
  `lock=${lockStatus.split(" ")[0]}`,
252
257
  `daemon=${daemonUp ? "running" : "stopped"}`,
253
258
  `orphaned=${orphanedProjects.length}`,
259
+ `stale_chunker=${staleChunkerProjects.length}`,
254
260
  ];
255
261
  console.log(fields.join("\t"));
262
+ if (staleChunkerProjects.length > 0) {
263
+ console.log(`stale_chunker_fix: run 'gmax index --reset' in: ${staleChunkerProjects
264
+ .map((p) => p.name || path.basename(p.root))
265
+ .join(", ")}`);
266
+ }
256
267
  }
257
268
  else {
258
269
  console.log("\nIndex Health\n");
@@ -296,6 +307,13 @@ exports.doctor = new commander_1.Command("doctor")
296
307
  }
297
308
  // Daemon
298
309
  console.log(`${daemonUp ? "ok" : "INFO"} Daemon: ${daemonUp ? "running" : "not running"}`);
310
+ // Index built by an older chunker — graph metadata fixes need a reindex
311
+ if (staleChunkerProjects.length > 0) {
312
+ console.log(`WARN Stale chunker: ${staleChunkerProjects.length} project(s) indexed before chunker v${config_1.CONFIG.CHUNKER_VERSION} — graph commands (peek/impact/trace) may overcount callers`);
313
+ for (const p of staleChunkerProjects) {
314
+ console.log(` - ${p.name || path.basename(p.root)}: run 'gmax index --reset' in ${p.root}`);
315
+ }
316
+ }
299
317
  // Projects
300
318
  if (orphanedProjects.length > 0) {
301
319
  console.log(`WARN Orphaned projects: ${orphanedProjects.length} (directories no longer exist)`);
@@ -46,6 +46,7 @@ exports.impact = void 0;
46
46
  const path = __importStar(require("node:path"));
47
47
  const commander_1 = require("commander");
48
48
  const impact_1 = require("../lib/graph/impact");
49
+ const test_hits_1 = require("../lib/graph/test-hits");
49
50
  const vector_db_1 = require("../lib/store/vector-db");
50
51
  const agent_errors_1 = require("../lib/utils/agent-errors");
51
52
  const exit_1 = require("../lib/utils/exit");
@@ -58,10 +59,13 @@ exports.impact = new commander_1.Command("impact")
58
59
  .option("--root <dir>", "Project root directory")
59
60
  .option("--in <subpath>", "Restrict to a sub-path of the project (repeatable)", (value, prev) => (prev ? [...prev, value] : [value]))
60
61
  .option("--exclude <subpath>", "Exclude a sub-path of the project (repeatable)", (value, prev) => (prev ? [...prev, value] : [value]))
62
+ .option("--no-tests", "Skip affected-test analysis; show production blast radius only")
61
63
  .option("--agent", "Compact output for AI agents", false)
62
64
  .action((target, opts) => __awaiter(void 0, void 0, void 0, function* () {
63
65
  var _a;
64
66
  const depth = Math.min(Math.max(Number.parseInt(opts.depth || "1", 10), 1), 3);
67
+ // commander maps --no-tests → opts.tests === false (defaults true).
68
+ const includeTests = opts.tests !== false;
65
69
  let vectorDb = null;
66
70
  try {
67
71
  const root = (0, project_registry_1.resolveRootOrExit)(opts.root);
@@ -96,21 +100,26 @@ exports.impact = new commander_1.Command("impact")
96
100
  const queryRoot = opts.in && opts.in.length > 0
97
101
  ? scope.pathPrefix.replace(/\/$/, "")
98
102
  : projectRoot;
99
- // Run dependents and tests in parallel
103
+ // Run dependents and tests in parallel. --no-tests skips the test
104
+ // traversal entirely so the affected-tests section is omitted (not just
105
+ // empty) below.
100
106
  const [dependents, tests] = yield Promise.all([
101
107
  (0, impact_1.findDependents)(symbols, vectorDb, queryRoot, excludePaths, undefined, scope.excludePrefixes),
102
- (0, impact_1.findTests)(symbols, vectorDb, queryRoot, depth, scope.excludePrefixes),
108
+ includeTests
109
+ ? (0, impact_1.findTests)(symbols, vectorDb, queryRoot, depth, scope.excludePrefixes)
110
+ : Promise.resolve([]),
103
111
  ]);
104
112
  // Separate test files from non-test dependents
105
113
  const nonTestDeps = dependents.filter((d) => !(0, impact_1.isTestPath)(d.file));
114
+ // One line per test file; caller symbols inside it become `via` detail.
115
+ const groupedTests = (0, test_hits_1.groupTestHitsByFile)(tests);
106
116
  const rel = (p) => p.startsWith(`${projectRoot}/`) ? p.slice(projectRoot.length + 1) : p;
107
117
  if (opts.agent) {
108
118
  for (const d of nonTestDeps) {
109
119
  console.log(`dep: ${rel(d.file)}\t${d.sharedSymbols}`);
110
120
  }
111
- for (const t of tests) {
112
- const hopLabel = t.hops === 0 ? "direct" : `${t.hops}-hop`;
113
- console.log(`test: ${rel(t.file)}:${t.line + 1}\t${t.symbol}\t${hopLabel}`);
121
+ for (const t of groupedTests) {
122
+ console.log(`test: ${rel(t.file)}:${t.line + 1}\t${(0, test_hits_1.hopLabelAgent)(t.hops)}${(0, test_hits_1.formatViaAgent)(t.via)}`);
114
123
  }
115
124
  if (!nonTestDeps.length && !tests.length) {
116
125
  console.log("(no impact detected)");
@@ -127,16 +136,17 @@ exports.impact = new commander_1.Command("impact")
127
136
  else {
128
137
  console.log("Direct dependents: none found");
129
138
  }
130
- console.log("");
131
- if (tests.length > 0) {
132
- console.log(`Affected tests (${tests.length}):`);
133
- for (const t of tests) {
134
- const hopLabel = t.hops === 0 ? "calls directly" : `${t.hops} hop${t.hops > 1 ? "s" : ""} away`;
135
- console.log(` ${rel(t.file)}:${t.line + 1} ${t.symbol} (${hopLabel})`);
139
+ if (includeTests) {
140
+ console.log("");
141
+ if (groupedTests.length > 0) {
142
+ console.log(`Affected tests (${groupedTests.length}):`);
143
+ for (const t of groupedTests) {
144
+ console.log(` ${rel(t.file)}:${t.line + 1} (${(0, test_hits_1.hopLabelHuman)(t.hops)}${(0, test_hits_1.formatViaHuman)(t.via)})`);
145
+ }
146
+ }
147
+ else {
148
+ console.log("Affected tests: none found");
136
149
  }
137
- }
138
- else {
139
- console.log("Affected tests: none found");
140
150
  }
141
151
  }
142
152
  }
@@ -45,6 +45,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
45
45
  exports.index = void 0;
46
46
  const path = __importStar(require("node:path"));
47
47
  const commander_1 = require("commander");
48
+ const config_1 = require("../config");
48
49
  const index_config_1 = require("../lib/index/index-config");
49
50
  const grammar_loader_1 = require("../lib/index/grammar-loader");
50
51
  const sync_helpers_1 = require("../lib/index/sync-helpers");
@@ -70,7 +71,7 @@ Examples:
70
71
  gmax index --reset Full re-index from scratch
71
72
  `)
72
73
  .action((_args, cmd) => __awaiter(void 0, void 0, void 0, function* () {
73
- var _a, _b, _c, _d;
74
+ var _a, _b, _c, _d, _e;
74
75
  const options = cmd.optsWithGlobals();
75
76
  let vectorDb = null;
76
77
  const ac = new AbortController();
@@ -90,11 +91,18 @@ Examples:
90
91
  : process.cwd();
91
92
  const projectRoot = (_a = (0, project_root_1.findProjectRoot)(indexRoot)) !== null && _a !== void 0 ? _a : indexRoot;
92
93
  // Project must be registered before reindexing
93
- if (!(0, project_registry_1.getProject)(projectRoot)) {
94
+ const existingEntry = (0, project_registry_1.getProject)(projectRoot);
95
+ if (!existingEntry) {
94
96
  console.error(`This project hasn't been added yet.\n\nRun: gmax add ${projectRoot}\n`);
95
97
  process.exitCode = 1;
96
98
  return;
97
99
  }
100
+ // Only a reset rechunks cached files, so only a reset may claim the
101
+ // current chunker version — a plain index would clear the doctor
102
+ // stale-chunker warning without regenerating any chunks.
103
+ const stampedChunkerVersion = options.reset
104
+ ? config_1.CONFIG.CHUNKER_VERSION
105
+ : ((_b = existingEntry.chunkerVersion) !== null && _b !== void 0 ? _b : 1);
98
106
  if (options.reset) {
99
107
  console.log("Resetting index...");
100
108
  }
@@ -115,7 +123,7 @@ Examples:
115
123
  });
116
124
  });
117
125
  if (!done.ok) {
118
- throw new Error((_b = done.error) !== null && _b !== void 0 ? _b : "daemon index failed");
126
+ throw new Error((_c = done.error) !== null && _c !== void 0 ? _c : "daemon index failed");
119
127
  }
120
128
  if (options.dryRun) {
121
129
  spinner.succeed(`Dry run complete(${done.processed} / ${done.total}) • would have indexed ${done.indexed} `);
@@ -129,10 +137,11 @@ Examples:
129
137
  modelTier: globalConfig.modelTier,
130
138
  embedMode: globalConfig.embedMode,
131
139
  lastIndexed: new Date().toISOString(),
132
- chunkCount: (_c = done.indexed) !== null && _c !== void 0 ? _c : 0,
140
+ chunkCount: (_d = done.indexed) !== null && _d !== void 0 ? _d : 0,
133
141
  status: "indexed",
142
+ chunkerVersion: stampedChunkerVersion,
134
143
  });
135
- const failedFiles = (_d = done.failedFiles) !== null && _d !== void 0 ? _d : 0;
144
+ const failedFiles = (_e = done.failedFiles) !== null && _e !== void 0 ? _e : 0;
136
145
  const failedSuffix = failedFiles > 0 ? ` • ${failedFiles} failed` : "";
137
146
  spinner.succeed(`Indexing complete(${done.processed} / ${done.total}) • indexed ${done.indexed}${failedSuffix} `);
138
147
  }
@@ -152,7 +161,7 @@ Examples:
152
161
  try {
153
162
  process.kill(watcher.pid, "SIGTERM");
154
163
  }
155
- catch (_e) { }
164
+ catch (_f) { }
156
165
  for (let i = 0; i < 50; i++) {
157
166
  if (!(0, watcher_store_1.isProcessRunning)(watcher.pid))
158
167
  break;
@@ -192,6 +201,7 @@ Examples:
192
201
  lastIndexed: new Date().toISOString(),
193
202
  chunkCount: result.indexed,
194
203
  status: "indexed",
204
+ chunkerVersion: stampedChunkerVersion,
195
205
  });
196
206
  const failedSuffix = result.failedFiles > 0 ? ` • ${result.failedFiles} failed` : "";
197
207
  spinner.succeed(`Indexing complete(${result.processed} / ${result.total}) • indexed ${result.indexed}${failedSuffix} `);
@@ -623,6 +623,7 @@ function formatMcpPointerSearchResults(data, displayRoot, options = {}) {
623
623
  return (0, agent_search_formatter_1.formatAgentSearchResults)(filterMcpSearchResults(data, options), displayRoot, {
624
624
  includeImports: options.includeImports,
625
625
  getImportsForFile: options.getImportsForFile,
626
+ query: options.query,
626
627
  });
627
628
  }
628
629
  // ---------------------------------------------------------------------------
@@ -834,6 +835,7 @@ exports.mcp = new commander_1.Command("mcp")
834
835
  namePattern,
835
836
  includeImports,
836
837
  getImportsForFile,
838
+ query,
837
839
  });
838
840
  if ((_a = result.warnings) === null || _a === void 0 ? void 0 : _a.length) {
839
841
  return ok(`${result.warnings.join("\n")}\n\n${output}`);
@@ -45,6 +45,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
45
45
  exports.peek = void 0;
46
46
  const fs = __importStar(require("node:fs"));
47
47
  const commander_1 = require("commander");
48
+ const callsites_1 = require("../lib/graph/callsites");
48
49
  const graph_builder_1 = require("../lib/graph/graph-builder");
49
50
  const vector_db_1 = require("../lib/store/vector-db");
50
51
  const agent_errors_1 = require("../lib/utils/agent-errors");
@@ -69,26 +70,45 @@ function extractSignature(filePath, startLine, endLine) {
69
70
  const lines = content.split("\n");
70
71
  const chunk = lines.slice(startLine, endLine + 1);
71
72
  const bodyLines = chunk.length;
72
- // Find the signature: everything up to and including the opening brace
73
+ // Find the signature: everything up to and including the opening brace.
74
+ // Only treat `{` / `=>` as the body boundary once the parameter list's
75
+ // parens are balanced — object-literal param types (`cached: { … }`)
76
+ // contain braces mid-signature and must not end it.
73
77
  const sigLines = [];
78
+ let parenDepth = 0;
74
79
  for (const line of chunk) {
75
80
  sigLines.push(line);
76
- if (line.includes("{") || line.includes("=>"))
81
+ for (const ch of line) {
82
+ if (ch === "(")
83
+ parenDepth++;
84
+ else if (ch === ")")
85
+ parenDepth--;
86
+ }
87
+ if (parenDepth <= 0 && (line.includes("{") || line.includes("=>"))) {
77
88
  break;
89
+ }
90
+ if (sigLines.length >= 12)
91
+ break; // degenerate input — bail
78
92
  }
79
93
  // If we only got one line and it's the whole function, collapse it
80
94
  if (sigLines.length >= bodyLines) {
81
- return { signature: chunk.join("\n"), bodyLines: 0 };
95
+ const whole = chunk.join("\n");
96
+ return { signature: whole, signatureOnly: whole, bodyLines: 0 };
82
97
  }
83
98
  const sig = sigLines.join("\n");
84
99
  const remaining = bodyLines - sigLines.length;
85
100
  return {
86
101
  signature: `${sig}\n // ... (${remaining} lines)\n }`,
102
+ signatureOnly: sig,
87
103
  bodyLines,
88
104
  };
89
105
  }
90
106
  catch (_a) {
91
- return { signature: "(source not available)", bodyLines: 0 };
107
+ return {
108
+ signature: "(source not available)",
109
+ signatureOnly: "(source not available)",
110
+ bodyLines: 0,
111
+ };
92
112
  }
93
113
  }
94
114
  exports.peek = new commander_1.Command("peek")
@@ -122,6 +142,9 @@ exports.peek = new commander_1.Command("peek")
122
142
  // languages, refuse to silently pick one. The graph builder otherwise
123
143
  // picks one chunk arbitrarily and lists callers from a different
124
144
  // language — verified failure mode.
145
+ // Same-language multi-definition is reported as a note instead (below):
146
+ // the first definition still wins, but the agent learns it guessed.
147
+ let otherDefs = [];
125
148
  {
126
149
  const tableForCheck = yield vectorDb.ensureTable();
127
150
  const allDefs = yield tableForCheck
@@ -134,6 +157,14 @@ exports.peek = new commander_1.Command("peek")
134
157
  path: String(row.path || ""),
135
158
  startLine: Number(row.start_line || 0),
136
159
  }));
160
+ // Dedupe by file: split sub-chunks of one definition share a path,
161
+ // while genuine ambiguity (same name defined elsewhere) crosses files.
162
+ const distinct = new Map();
163
+ for (const c of chunks) {
164
+ if (!distinct.has(c.path))
165
+ distinct.set(c.path, c);
166
+ }
167
+ otherDefs = [...distinct.values()];
137
168
  const byLang = (0, language_1.groupByLanguage)(chunks);
138
169
  if (byLang.size >= 2) {
139
170
  const rel = (p) => p.startsWith(projectRoot) ? p.slice(projectRoot.length + 1) : p;
@@ -198,7 +229,30 @@ exports.peek = new commander_1.Command("peek")
198
229
  line: c.line,
199
230
  }));
200
231
  }
201
- const calleeList = graph.callees.map((c) => ({
232
+ // Re-anchor chunk-level caller rows to actual call sites and dedupe —
233
+ // getCallers() returns one row per chunk, which multiplies callers for
234
+ // classes split across many chunks (verified: 3 real call sites → 66).
235
+ const resolvedCallers = (0, callsites_1.resolveCallSites)(callerList, symbol).map((c) => {
236
+ var _a;
237
+ return ({
238
+ symbol: c.symbol,
239
+ file: c.file,
240
+ line: (_a = c.snippetLine) !== null && _a !== void 0 ? _a : c.line,
241
+ });
242
+ });
243
+ // Builtins listed as "(not indexed)" callees (trunc, now, filter, …)
244
+ // are noise; project symbols always resolve so they're unaffected.
245
+ // Dedupe by symbol — repeated references arrive once per chunk.
246
+ const seenCallees = new Set();
247
+ const calleeList = graph.callees
248
+ .filter((c) => c.file || !(0, callsites_1.isBuiltinCallee)(c.symbol))
249
+ .filter((c) => {
250
+ if (seenCallees.has(c.symbol))
251
+ return false;
252
+ seenCallees.add(c.symbol);
253
+ return true;
254
+ })
255
+ .map((c) => ({
202
256
  symbol: c.symbol,
203
257
  file: c.file,
204
258
  line: c.line,
@@ -207,16 +261,30 @@ exports.peek = new commander_1.Command("peek")
207
261
  // Compact TSV output
208
262
  const exportedStr = exported ? "exported" : "";
209
263
  console.log(`${center.symbol}\t${rel(center.file)}:${center.line + 1}\t${center.role}\t${exportedStr}`);
210
- // Signature (first line only)
211
- const { signature } = extractSignature(center.file, startLine, endLine);
212
- const firstLine = signature.split("\n")[0].trim();
213
- console.log(`sig: ${firstLine}`);
264
+ if (otherDefs.length > 1) {
265
+ const others = otherDefs
266
+ .filter((d) => d.path !== center.file)
267
+ .slice(0, 4)
268
+ .map((d) => `${rel(d.path)}:${d.startLine + 1}`);
269
+ if (others.length > 0) {
270
+ console.log(`also-defined: ${others.join(", ")} — showing the first; pin with --in <subpath>`);
271
+ }
272
+ }
273
+ // Signature — all lines up to the opening brace, collapsed to one
274
+ // line so parameters survive (first-line-only loses them).
275
+ const { signatureOnly } = extractSignature(center.file, startLine, endLine);
276
+ const sigOnly = signatureOnly
277
+ .split("\n")
278
+ .map((l) => l.trim())
279
+ .join(" ")
280
+ .replace(/\s+/g, " ");
281
+ console.log(`sig: ${sigOnly}`);
214
282
  // Callers
215
- for (const c of callerList.slice(0, MAX_CALLERS)) {
283
+ for (const c of resolvedCallers.slice(0, MAX_CALLERS)) {
216
284
  console.log(`<- ${c.symbol}\t${c.file ? `${rel(c.file)}:${c.line + 1}` : "(not indexed)"}`);
217
285
  }
218
- if (callerList.length > MAX_CALLERS) {
219
- console.log(`<- ... ${callerList.length - MAX_CALLERS} more`);
286
+ if (resolvedCallers.length > MAX_CALLERS) {
287
+ console.log(`<- ... ${resolvedCallers.length - MAX_CALLERS} more`);
220
288
  }
221
289
  // Callees
222
290
  for (const c of calleeList.slice(0, MAX_CALLEES)) {
@@ -239,6 +307,15 @@ exports.peek = new commander_1.Command("peek")
239
307
  // Rich output
240
308
  const exportedStr = exported ? ", exported" : "";
241
309
  console.log(`${style.bold(`peek: ${center.symbol}`)} ${style.dim(`${rel(center.file)}:${center.line + 1}`)} ${style.dim(`[${center.role}${exportedStr}]`)}`);
310
+ if (otherDefs.length > 1) {
311
+ const others = otherDefs
312
+ .filter((d) => d.path !== center.file)
313
+ .slice(0, 4)
314
+ .map((d) => `${rel(d.path)}:${d.startLine + 1}`);
315
+ if (others.length > 0) {
316
+ console.log(style.dim(` also defined in: ${others.join(", ")} — showing the first; pin with --in <subpath>`));
317
+ }
318
+ }
242
319
  console.log();
243
320
  // Signature with collapsed body
244
321
  const { signature } = extractSignature(center.file, startLine, endLine);
@@ -247,9 +324,9 @@ exports.peek = new commander_1.Command("peek")
247
324
  }
248
325
  console.log();
249
326
  // Callers
250
- if (callerList.length > 0) {
251
- const shown = callerList.slice(0, MAX_CALLERS);
252
- console.log(style.bold(`callers (${callerList.length}):`));
327
+ if (resolvedCallers.length > 0) {
328
+ const shown = resolvedCallers.slice(0, MAX_CALLERS);
329
+ console.log(style.bold(`callers (${resolvedCallers.length}):`));
253
330
  for (const c of shown) {
254
331
  if (c.file) {
255
332
  console.log(` ${style.blue("\u2190")} ${style.green(c.symbol.padEnd(25))} ${style.dim(`${rel(c.file)}:${c.line + 1}`)}`);
@@ -258,8 +335,8 @@ exports.peek = new commander_1.Command("peek")
258
335
  console.log(` ${style.blue("\u2190")} ${c.symbol.padEnd(25)} ${style.dim("(not indexed)")}`);
259
336
  }
260
337
  }
261
- if (callerList.length > MAX_CALLERS) {
262
- console.log(style.dim(` ... and ${callerList.length - MAX_CALLERS} more`));
338
+ if (resolvedCallers.length > MAX_CALLERS) {
339
+ console.log(style.dim(` ... and ${resolvedCallers.length - MAX_CALLERS} more`));
263
340
  }
264
341
  }
265
342
  else {
@@ -45,6 +45,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
45
45
  exports.project = void 0;
46
46
  const path = __importStar(require("node:path"));
47
47
  const commander_1 = require("commander");
48
+ const callsites_1 = require("../lib/graph/callsites");
48
49
  const vector_db_1 = require("../lib/store/vector-db");
49
50
  const filter_builder_1 = require("../lib/utils/filter-builder");
50
51
  const exit_1 = require("../lib/utils/exit");
@@ -91,7 +92,9 @@ exports.project = new commander_1.Command("project")
91
92
  const dirCounts = new Map();
92
93
  const roleCounts = new Map();
93
94
  const symbolRefs = new Map();
95
+ const definedInProject = new Set();
94
96
  const entryPoints = [];
97
+ const seenEntryPoints = new Set();
95
98
  for (const row of rows) {
96
99
  const p = String(row.path || "");
97
100
  const role = String(row.role || "IMPLEMENTATION");
@@ -115,13 +118,19 @@ exports.project = new commander_1.Command("project")
115
118
  dc.files.add(p);
116
119
  dc.chunks++;
117
120
  roleCounts.set(role, (roleCounts.get(role) || 0) + 1);
121
+ for (const d of defs)
122
+ definedInProject.add(d);
118
123
  for (const ref of refs)
119
124
  symbolRefs.set(ref, (symbolRefs.get(ref) || 0) + 1);
120
125
  if (exported && role === "ORCHESTRATION" && complexity >= 5 && defs.length > 0) {
121
- entryPoints.push({
122
- symbol: defs[0],
123
- path: p.startsWith(prefix) ? p.slice(prefix.length) : p,
124
- });
126
+ const epKey = `${defs[0]}:${p}`;
127
+ if (!seenEntryPoints.has(epKey)) {
128
+ seenEntryPoints.add(epKey);
129
+ entryPoints.push({
130
+ symbol: defs[0],
131
+ path: p.startsWith(prefix) ? p.slice(prefix.length) : p,
132
+ });
133
+ }
125
134
  }
126
135
  }
127
136
  const projects = (0, project_registry_1.listProjects)();
@@ -129,7 +138,11 @@ exports.project = new commander_1.Command("project")
129
138
  const extEntries = Array.from(extCounts.entries())
130
139
  .sort((a, b) => b[1] - a[1])
131
140
  .slice(0, 8);
141
+ // Key symbols must be the project's own: raw referenced_symbols counts
142
+ // are dominated by JS builtins (push, slice, map, …) which say nothing
143
+ // about the codebase.
132
144
  const topSymbols = Array.from(symbolRefs.entries())
145
+ .filter(([s]) => definedInProject.has(s) && !(0, callsites_1.isBuiltinCallee)(s))
133
146
  .sort((a, b) => b[1] - a[1])
134
147
  .slice(0, 8);
135
148
  if (opts.agent) {