cgraphx 1.1.0 → 1.2.0

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.
Files changed (81) hide show
  1. package/README.md +0 -1
  2. package/dist/.claude-template/skills/cgraphx/SKILL.md +3 -3
  3. package/dist/.claude-template/skills/cgraphx/agent-prompt.md +1 -1
  4. package/dist/.claude-template/skills/cgraphx-guide/SKILL.md +94 -0
  5. package/dist/.claude-template/skills/cgraphx-guide/how-to-use.html +403 -0
  6. package/dist/.claude-template/skills/clarify-requirements/SKILL.md +19 -8
  7. package/dist/.claude-template/skills/code-impact-docgen/SKILL.md +186 -176
  8. package/dist/.claude-template/skills/code-impact-docgen/template-design-html.md +357 -0
  9. package/dist/.claude-template/skills/code-impact-docgen/template-design-md.md +164 -0
  10. package/dist/.claude-template/skills/code-impact-init/SKILL.md +47 -47
  11. package/dist/.claude-template/skills/developer-timeline/SKILL.md +9 -0
  12. package/dist/.claude-template/skills/write-api-doc/SKILL.md +317 -0
  13. package/dist/.claude-template/skills/write-api-doc/template-api-html.md +422 -0
  14. package/dist/.claude-template/skills/write-plan/SKILL.md +38 -16
  15. package/dist/.claude-template/skills/write-prd/SKILL.md +32 -8
  16. package/dist/.claude-template/skills/write-spec/SKILL.md +34 -9
  17. package/dist/bin/codegraph.js +0 -100
  18. package/dist/bin/codegraph.js.map +1 -1
  19. package/dist/resolution/index.d.ts.map +1 -1
  20. package/dist/resolution/index.js +13 -0
  21. package/dist/resolution/index.js.map +1 -1
  22. package/dist/resolution/scope-index.d.ts +86 -0
  23. package/dist/resolution/scope-index.d.ts.map +1 -0
  24. package/dist/resolution/scope-index.js +143 -0
  25. package/dist/resolution/scope-index.js.map +1 -0
  26. package/dist/resolution/stdlib-blocklist.d.ts +53 -0
  27. package/dist/resolution/stdlib-blocklist.d.ts.map +1 -0
  28. package/dist/resolution/stdlib-blocklist.js +143 -0
  29. package/dist/resolution/stdlib-blocklist.js.map +1 -0
  30. package/dist/search/ast-helpers.d.ts +42 -0
  31. package/dist/search/ast-helpers.d.ts.map +1 -0
  32. package/dist/search/ast-helpers.js +106 -0
  33. package/dist/search/ast-helpers.js.map +1 -0
  34. package/dist/search/call-sites.d.ts +398 -0
  35. package/dist/search/call-sites.d.ts.map +1 -0
  36. package/dist/search/call-sites.js +1433 -0
  37. package/dist/search/call-sites.js.map +1 -0
  38. package/dist/search/context.d.ts +134 -0
  39. package/dist/search/context.d.ts.map +1 -0
  40. package/dist/search/context.js +575 -0
  41. package/dist/search/context.js.map +1 -0
  42. package/dist/search/impact.d.ts +139 -0
  43. package/dist/search/impact.d.ts.map +1 -0
  44. package/dist/search/impact.js +646 -0
  45. package/dist/search/impact.js.map +1 -0
  46. package/dist/search/related.d.ts +178 -0
  47. package/dist/search/related.d.ts.map +1 -0
  48. package/dist/search/related.js +667 -0
  49. package/dist/search/related.js.map +1 -0
  50. package/dist/search/slice.d.ts +148 -0
  51. package/dist/search/slice.d.ts.map +1 -0
  52. package/dist/search/slice.js +460 -0
  53. package/dist/search/slice.js.map +1 -0
  54. package/dist/search/snr-constants.d.ts +41 -0
  55. package/dist/search/snr-constants.d.ts.map +1 -0
  56. package/dist/search/snr-constants.js +44 -0
  57. package/dist/search/snr-constants.js.map +1 -0
  58. package/dist/search/types.d.ts +28 -0
  59. package/dist/search/types.d.ts.map +1 -0
  60. package/dist/search/types.js +12 -0
  61. package/dist/search/types.js.map +1 -0
  62. package/dist/timeline/cli.d.ts.map +1 -1
  63. package/dist/timeline/cli.js +22 -3
  64. package/dist/timeline/cli.js.map +1 -1
  65. package/dist/timeline/store.d.ts +5 -0
  66. package/dist/timeline/store.d.ts.map +1 -1
  67. package/dist/timeline/store.js +23 -3
  68. package/dist/timeline/store.js.map +1 -1
  69. package/package.json +1 -1
  70. package/scripts/agent-eval/block-cgraphx-and-gitnexus-cli-hook.sh +43 -0
  71. package/scripts/agent-eval/block-cgraphx-cli-hook.sh +32 -0
  72. package/scripts/agent-eval/block-cgraphx-cli-settings.json +16 -0
  73. package/scripts/agent-eval/cli-vs-mcp-3arm.sh +121 -0
  74. package/scripts/agent-eval/multi-tool-eval.sh +171 -0
  75. package/scripts/agent-eval/parse-cli-vs-mcp.mjs +232 -0
  76. package/scripts/agent-eval/parse-multi-tool.mjs +242 -0
  77. package/scripts/agent-eval/subagent-token-cost.py +188 -0
  78. package/dist/.claude-template/skills/code-impact-docgen/template-business-html.md +0 -242
  79. package/dist/.claude-template/skills/code-impact-docgen/template-business-md.md +0 -107
  80. package/dist/.claude-template/skills/code-impact-docgen/template-technical-html.md +0 -205
  81. package/dist/.claude-template/skills/code-impact-docgen/template-technical-md.md +0 -155
@@ -0,0 +1,1433 @@
1
+ "use strict";
2
+ /**
3
+ * Call site search — finds method-invocation sites by name across the project.
4
+ *
5
+ * Designed as an agent-facing "structured grep": returns call sites with AST
6
+ * context (containing class/method), not raw text matches. AST naturally
7
+ * excludes comments/strings (they're not method_invocation nodes), so the
8
+ * output is signal-dense for agent consumption — saves the agent the
9
+ * comment/string/overload filtering that grep forces it to do.
10
+ *
11
+ * Does NOT depend on cgraphx's resolve pipeline. Walks source files directly
12
+ * via tree-sitter, so it works on any project (indexed or not) and avoids the
13
+ * resolve-accuracy issues that broke L3 scope-bounded on Java dotted names.
14
+ */
15
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
16
+ if (k2 === undefined) k2 = k;
17
+ var desc = Object.getOwnPropertyDescriptor(m, k);
18
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
19
+ desc = { enumerable: true, get: function() { return m[k]; } };
20
+ }
21
+ Object.defineProperty(o, k2, desc);
22
+ }) : (function(o, m, k, k2) {
23
+ if (k2 === undefined) k2 = k;
24
+ o[k2] = m[k];
25
+ }));
26
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
27
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
28
+ }) : function(o, v) {
29
+ o["default"] = v;
30
+ });
31
+ var __importStar = (this && this.__importStar) || (function () {
32
+ var ownKeys = function(o) {
33
+ ownKeys = Object.getOwnPropertyNames || function (o) {
34
+ var ar = [];
35
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
36
+ return ar;
37
+ };
38
+ return ownKeys(o);
39
+ };
40
+ return function (mod) {
41
+ if (mod && mod.__esModule) return mod;
42
+ var result = {};
43
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
44
+ __setModuleDefault(result, mod);
45
+ return result;
46
+ };
47
+ })();
48
+ Object.defineProperty(exports, "__esModule", { value: true });
49
+ exports.DEFAULT_EXCLUDE_DIRS = void 0;
50
+ exports.searchCallSites = searchCallSites;
51
+ exports.compareCallSitesTiebreak = compareCallSitesTiebreak;
52
+ exports.listJavaFamilyFiles = listJavaFamilyFiles;
53
+ exports.formatCallSitesResult = formatCallSitesResult;
54
+ exports.resolveMethodSymbol = resolveMethodSymbol;
55
+ exports.findEnclosingClassType = findEnclosingClassType;
56
+ exports.extractInjectedFields = extractInjectedFields;
57
+ exports.findFilesWithClass = findFilesWithClass;
58
+ exports.containsBizVerbSegment = containsBizVerbSegment;
59
+ exports.rootIdentifier = rootIdentifier;
60
+ exports.computeSnrScore = computeSnrScore;
61
+ exports.computeSnrBreakdown = computeSnrBreakdown;
62
+ exports.formatSnrBreakdown = formatSnrBreakdown;
63
+ exports.compareBySnr = compareBySnr;
64
+ exports.collectSameClassMethodNames = collectSameClassMethodNames;
65
+ exports.markSameClassBiz = markSameClassBiz;
66
+ exports.searchCallees = searchCallees;
67
+ exports.applyTieredBudget = applyTieredBudget;
68
+ exports.formatCalleesResult = formatCalleesResult;
69
+ const fs_1 = require("fs");
70
+ const path = __importStar(require("path"));
71
+ const grammars_1 = require("../extraction/grammars");
72
+ const ast_helpers_1 = require("./ast-helpers");
73
+ const snr_constants_1 = require("./snr-constants");
74
+ // ---------------------------------------------------------------------------
75
+ // Built-in file-scan exclusions (targeted at Java/JVM build output + noise)
76
+ // ---------------------------------------------------------------------------
77
+ exports.DEFAULT_EXCLUDE_DIRS = new Set([
78
+ '.git',
79
+ '.cgraphx',
80
+ '.idea',
81
+ '.vscode',
82
+ 'node_modules',
83
+ 'target', // Maven
84
+ 'build', // Gradle
85
+ 'out', // IntelliJ out
86
+ '.gradle',
87
+ '.mvn',
88
+ 'dist',
89
+ '__pycache__',
90
+ '.cache',
91
+ ]);
92
+ const JAVA_FAMILY_EXTENSIONS = new Set(['.java', '.kt', '.kts']);
93
+ // ---------------------------------------------------------------------------
94
+ // Main entry
95
+ // ---------------------------------------------------------------------------
96
+ async function searchCallSites(projectRoot, methodName, options = {}) {
97
+ const { topK = 20, topPerReceiver = 3, budget = 4000, receiver, languages = ['java', 'kotlin'], excludeDirs = [], } = options;
98
+ // initGrammars() only initializes the WASM runtime; loadGrammarsForLanguages
99
+ // actually loads the Java/Kotlin grammar WASM. Without this, getParser()
100
+ // returns null and we silently find 0 call sites.
101
+ await (0, grammars_1.loadGrammarsForLanguages)(languages);
102
+ const excludeSet = new Set([...exports.DEFAULT_EXCLUDE_DIRS, ...excludeDirs]);
103
+ // 1. Walk project, collect Java-family files
104
+ const allFiles = await listJavaFamilyFiles(projectRoot, excludeSet);
105
+ // 2. Pre-parse text filter: only parse files that contain methodName as text.
106
+ // Reading the file is unavoidable (we need content to parse), so we do the
107
+ // text check on the same read and skip parse if no match.
108
+ const candidateFiles = [];
109
+ let skippedFiles = 0;
110
+ for (const file of allFiles) {
111
+ let content;
112
+ try {
113
+ content = await fs_1.promises.readFile(file, 'utf-8');
114
+ }
115
+ catch {
116
+ skippedFiles++;
117
+ continue;
118
+ }
119
+ if (!content.includes(methodName)) {
120
+ skippedFiles++;
121
+ continue;
122
+ }
123
+ const language = (0, grammars_1.detectLanguage)(file, content);
124
+ if (!languages.includes(language)) {
125
+ skippedFiles++;
126
+ continue;
127
+ }
128
+ candidateFiles.push({ file, content, language });
129
+ }
130
+ // 3. Parse each candidate + extract method_invocation nodes matching methodName.
131
+ // extractMethodInvocationsWithSnr walks the tree once per file and computes
132
+ // xBdy / sameClassBiz / snrScore inline (mirrors the callees path) so the
133
+ // downstream cluster + sort can be SNR-driven. Injected-field and sibling-
134
+ // method lookups are cached per enclosing class node to avoid re-walking
135
+ // the same class body for every method_invocation inside it.
136
+ const allCallSites = [];
137
+ for (const { file, content, language } of candidateFiles) {
138
+ const parser = (0, grammars_1.getParser)(language);
139
+ if (!parser)
140
+ continue;
141
+ const tree = parser.parse(content);
142
+ if (!tree)
143
+ continue;
144
+ const sites = extractMethodInvocationsWithSnr(tree.rootNode, content, methodName, projectRoot, file);
145
+ allCallSites.push(...sites);
146
+ }
147
+ // 4. Receiver filter (optional)
148
+ let filtered = allCallSites;
149
+ let filteredByReceiver = 0;
150
+ if (receiver) {
151
+ const before = filtered.length;
152
+ filtered = filtered.filter((s) => s.receiverText === receiver);
153
+ filteredByReceiver = before - filtered.length;
154
+ }
155
+ // 5. Cluster by receiver (high-frequency methods get called by many
156
+ // receivers; without clustering, topK returns only the most-common
157
+ // receiver's first K calls — agent sees 20 CollectionUtils.isNotEmpty
158
+ // and misses StringUtils.isNotEmpty). Each receiver gets up to
159
+ // `topPerReceiver` representatives, capped by global `topK`.
160
+ const distinctReceivers = new Set(filtered.map((s) => s.receiverText)).size;
161
+ const byReceiver = new Map();
162
+ for (const site of filtered) {
163
+ const key = site.receiverText;
164
+ const group = byReceiver.get(key) ?? [];
165
+ group.push(site);
166
+ byReceiver.set(key, group);
167
+ }
168
+ // Sort each group internally by SNR desc (was: confidence > path depth).
169
+ // Same ranking as callees mode — `compareBySnr` tiebreaks on filePath depth
170
+ // asc + line asc per spec §2.4.
171
+ for (const group of byReceiver.values()) {
172
+ group.sort(compareBySnr);
173
+ }
174
+ // Receivers ordered by their top caller's SNR score (was: group size desc).
175
+ // xBdy callers score +100 so xBdy groups naturally rise to the top; a
176
+ // receiver like `customerOrderService` (injected service field) outranks
177
+ // `Objects`/`CollectionUtils` noise even when the noise group is larger.
178
+ const sortedReceivers = [...byReceiver.entries()].sort((a, b) => {
179
+ const aTop = a[1][0]?.snrScore ?? -Infinity;
180
+ const bTop = b[1][0]?.snrScore ?? -Infinity;
181
+ if (aTop !== bTop)
182
+ return bTop - aTop;
183
+ return b[1].length - a[1].length; // tiebreak: larger group first
184
+ });
185
+ const receivers = [];
186
+ const topSites = [];
187
+ for (const [receiverKey, group] of sortedReceivers) {
188
+ const maxSnrScore = group[0]?.snrScore;
189
+ if (topSites.length >= topK) {
190
+ receivers.push({ receiver: receiverKey, total: group.length, returned: 0, maxSnrScore });
191
+ continue;
192
+ }
193
+ const take = Math.min(topPerReceiver, group.length, topK - topSites.length);
194
+ topSites.push(...group.slice(0, take));
195
+ receivers.push({ receiver: receiverKey, total: group.length, returned: take, maxSnrScore });
196
+ }
197
+ const truncatedByTopK = Math.max(0, filtered.length - topSites.length);
198
+ // 7. Budget (estimated by JSON size per site)
199
+ let usedBudget = 0;
200
+ const budgeted = [];
201
+ let truncatedByBudget = 0;
202
+ for (const site of topSites) {
203
+ const siteSize = estimateSiteSize(site);
204
+ if (usedBudget + siteSize > budget && budgeted.length > 0) {
205
+ truncatedByBudget = topSites.length - budgeted.length;
206
+ break;
207
+ }
208
+ budgeted.push(site);
209
+ usedBudget += siteSize;
210
+ }
211
+ return {
212
+ query: methodName,
213
+ kind: 'call',
214
+ totalFound: allCallSites.length,
215
+ returned: budgeted.length,
216
+ distinctReceivers,
217
+ receivers,
218
+ omitted: {
219
+ commentMatches: 0,
220
+ stringMatches: 0,
221
+ skippedFiles,
222
+ filteredByReceiver,
223
+ truncatedByTopK,
224
+ truncatedByBudget,
225
+ },
226
+ callSites: budgeted,
227
+ };
228
+ }
229
+ // ---------------------------------------------------------------------------
230
+ // AST extraction
231
+ // ---------------------------------------------------------------------------
232
+ /**
233
+ * Walk the tree, find `method_invocation` nodes whose `name` field matches,
234
+ * and build CallSite records with enclosing class/method context.
235
+ *
236
+ * Java/Kotlin grammar uses `method_invocation` with `object` (receiver) and
237
+ * `name` (method) fields — see cgraphx/src/extraction/tree-sitter.ts:3440.
238
+ */
239
+ function extractMethodInvocations(root, content, methodName, projectRoot, filePath) {
240
+ const sites = [];
241
+ const relativePath = path.relative(projectRoot, filePath).replace(/\\/g, '/');
242
+ (0, ast_helpers_1.walkTree)(root, (node) => {
243
+ if (node.type !== 'method_invocation')
244
+ return;
245
+ const nameNode = node.childForFieldName('name');
246
+ if (!nameNode)
247
+ return;
248
+ // methodName === null = wildcard (callees mode collects all invocations
249
+ // regardless of name; callers mode filters to a specific name).
250
+ if (methodName !== null && nameNode.text !== methodName)
251
+ return;
252
+ const objectNode = node.childForFieldName('object');
253
+ const receiverText = objectNode?.text ?? null;
254
+ sites.push({
255
+ methodName: nameNode.text,
256
+ receiverText,
257
+ filePath: relativePath,
258
+ line: node.startPosition.row + 1,
259
+ column: node.startPosition.column,
260
+ containingClass: findAncestorName(node, [
261
+ 'class_declaration',
262
+ 'interface_declaration',
263
+ 'enum_declaration',
264
+ 'record_declaration',
265
+ 'annotation_type_declaration',
266
+ ]),
267
+ containingMethod: findAncestorName(node, [
268
+ 'method_declaration',
269
+ 'constructor_declaration',
270
+ 'compact_constructor_declaration',
271
+ ]),
272
+ snippet: extractSnippet(content, node.startPosition.row, node.endPosition.row),
273
+ confidence: receiverText ? 'high' : 'probable',
274
+ reason: receiverText
275
+ ? `method invocation with receiver '${receiverText}'`
276
+ : 'unqualified method invocation (no receiver — likely same-class or static)',
277
+ });
278
+ });
279
+ return sites;
280
+ }
281
+ function findAncestorName(node, ancestorTypes) {
282
+ let cur = node.parent;
283
+ while (cur) {
284
+ if (ancestorTypes.includes(cur.type)) {
285
+ const nameChild = cur.childForFieldName('name');
286
+ if (nameChild)
287
+ return nameChild.text;
288
+ }
289
+ cur = cur.parent;
290
+ }
291
+ return null;
292
+ }
293
+ /**
294
+ * Same shape as `extractMethodInvocations` (same pre-order walk, same CallSite
295
+ * fields), but additionally computes `crossBoundary`, `sameClassBiz`, and
296
+ * `snrScore` on each site — used by callers mode (`searchCallSites`) so the
297
+ * downstream cluster + sort can be SNR-driven, identical to callees mode.
298
+ *
299
+ * The callees path (`searchCallees`) resolves a single method body and enriches
300
+ * inline; this variant is for the callers path which walks the whole project
301
+ * and needs per-file, per-class enrichment.
302
+ *
303
+ * Injected-field maps and sibling-method-name sets are cached per enclosing
304
+ * class node (a single file may host many classes — outer, inner, nested — and
305
+ * every method_invocation inside one class shares its body's lookup).
306
+ */
307
+ function extractMethodInvocationsWithSnr(root, content, methodName, projectRoot, filePath) {
308
+ const sites = [];
309
+ const relativePath = path.relative(projectRoot, filePath).replace(/\\/g, '/');
310
+ const injectedFieldsByClass = new WeakMap();
311
+ const siblingNamesByClass = new WeakMap();
312
+ (0, ast_helpers_1.walkTree)(root, (node) => {
313
+ if (node.type !== 'method_invocation')
314
+ return;
315
+ const nameNode = node.childForFieldName('name');
316
+ if (!nameNode)
317
+ return;
318
+ if (methodName !== null && nameNode.text !== methodName)
319
+ return;
320
+ const objectNode = node.childForFieldName('object');
321
+ const receiverText = objectNode?.text ?? null;
322
+ // Resolve the enclosing class once for this call site, then look up (or
323
+ // populate) the per-class caches for injected fields + sibling method names.
324
+ const classNode = findEnclosingClassType(node);
325
+ let injectedFields;
326
+ let siblingNames;
327
+ if (classNode) {
328
+ injectedFields = injectedFieldsByClass.get(classNode);
329
+ if (!injectedFields) {
330
+ injectedFields = extractInjectedFields(classNode);
331
+ injectedFieldsByClass.set(classNode, injectedFields);
332
+ }
333
+ siblingNames = siblingNamesByClass.get(classNode);
334
+ if (!siblingNames) {
335
+ siblingNames = collectSameClassMethodNames(classNode);
336
+ siblingNamesByClass.set(classNode, siblingNames);
337
+ }
338
+ }
339
+ // xBdy: receiver matches an injected field declared in the enclosing class.
340
+ // (Symmetric to the callees path — a call like `customerOrderService.sceneCommit()`
341
+ // where `customerOrderService` is an @Resource/@Autowired/@Inject field crosses
342
+ // a module/service boundary and is a high-signal hop for tracing.)
343
+ let crossBoundary;
344
+ if (receiverText && injectedFields) {
345
+ const injected = injectedFields.get(receiverText);
346
+ if (injected) {
347
+ crossBoundary = {
348
+ kind: 'injected',
349
+ fieldType: injected.fieldType,
350
+ annotation: injected.annotation,
351
+ };
352
+ }
353
+ }
354
+ // sameClassBiz: unqualified call whose name is a biz-verb declared as a
355
+ // sibling method in the same class. Only checked when NOT xBdy (xBdy
356
+ // short-circuits in computeSnrScore anyway, and we don't want to falsely
357
+ // tag an xBdy field call as sameClassBiz).
358
+ let sameClassBiz;
359
+ if (!crossBoundary &&
360
+ (receiverText === null || receiverText === '') &&
361
+ siblingNames &&
362
+ containsBizVerbSegment(nameNode.text) &&
363
+ siblingNames.has(nameNode.text)) {
364
+ sameClassBiz = true;
365
+ }
366
+ const site = {
367
+ methodName: nameNode.text,
368
+ receiverText,
369
+ filePath: relativePath,
370
+ line: node.startPosition.row + 1,
371
+ column: node.startPosition.column,
372
+ containingClass: findAncestorName(node, [
373
+ 'class_declaration',
374
+ 'interface_declaration',
375
+ 'enum_declaration',
376
+ 'record_declaration',
377
+ 'annotation_type_declaration',
378
+ ]),
379
+ containingMethod: findAncestorName(node, [
380
+ 'method_declaration',
381
+ 'constructor_declaration',
382
+ 'compact_constructor_declaration',
383
+ ]),
384
+ snippet: extractSnippet(content, node.startPosition.row, node.endPosition.row),
385
+ confidence: receiverText ? 'high' : 'probable',
386
+ reason: receiverText
387
+ ? `method invocation with receiver '${receiverText}'`
388
+ : 'unqualified method invocation (no receiver — likely same-class or static)',
389
+ };
390
+ if (crossBoundary)
391
+ site.crossBoundary = crossBoundary;
392
+ if (sameClassBiz)
393
+ site.sameClassBiz = sameClassBiz;
394
+ site.snrScore = computeSnrScore(site);
395
+ sites.push(site);
396
+ });
397
+ return sites;
398
+ }
399
+ function extractSnippet(content, startRow, endRow) {
400
+ const lines = content.split('\n');
401
+ // Cap snippet at 3 lines; method_invocation usually fits on 1-2 lines.
402
+ const maxLines = 3;
403
+ const sliceEnd = Math.min(endRow + 1, startRow + maxLines);
404
+ const snippetLines = lines.slice(startRow, sliceEnd);
405
+ return snippetLines.join('\n').trim();
406
+ }
407
+ // ---------------------------------------------------------------------------
408
+ // Ranking + budgeting
409
+ // ---------------------------------------------------------------------------
410
+ /**
411
+ * Tiebreaker-only comparator for callers-mode call sites.
412
+ *
413
+ * Previously this was the primary ranking for `searchCallSites` (confidence +
414
+ * path-depth, as `compareCallSites`). After iteration-3 B-2 callers switched
415
+ * to SNR-driven ranking via `compareBySnr`, the SNR tiebreaker chain
416
+ * (filePath depth asc + filePath localeCompare + line asc, per spec §2.4)
417
+ * already covers every case this comparator handled, so the callers path no
418
+ * longer calls it.
419
+ *
420
+ * Exported as a pure tiebreaker for any external/manual sort that wants a
421
+ * stable order without caring about SNR — symmetric to `compareBySnr` minus
422
+ * the SNR primary key.
423
+ */
424
+ function compareCallSitesTiebreak(a, b) {
425
+ // Shallower path = likely more central (controller/service > impl/detail)
426
+ const aDepth = a.filePath.split('/').length;
427
+ const bDepth = b.filePath.split('/').length;
428
+ if (aDepth !== bDepth)
429
+ return aDepth - bDepth;
430
+ // Stable tiebreak by file:line
431
+ if (a.filePath !== b.filePath)
432
+ return a.filePath.localeCompare(b.filePath);
433
+ return a.line - b.line;
434
+ }
435
+ function estimateSiteSize(site) {
436
+ // Rough: snippet dominates. Count snippet + fixed overhead per field.
437
+ return site.snippet.length + 200;
438
+ }
439
+ // ---------------------------------------------------------------------------
440
+ // File walk
441
+ // ---------------------------------------------------------------------------
442
+ async function listJavaFamilyFiles(projectRoot, excludeDirs) {
443
+ const results = [];
444
+ await walkDir(projectRoot, projectRoot, excludeDirs, results);
445
+ return results;
446
+ }
447
+ async function walkDir(root, currentDir, excludeDirs, results) {
448
+ let entries;
449
+ try {
450
+ entries = await fs_1.promises.readdir(currentDir, { withFileTypes: true });
451
+ }
452
+ catch {
453
+ return;
454
+ }
455
+ for (const entry of entries) {
456
+ if (entry.isDirectory()) {
457
+ if (excludeDirs.has(entry.name))
458
+ continue;
459
+ // Skip hidden dirs (except none — we exclude all starting with '.')
460
+ if (entry.name.startsWith('.') && entry.name !== '.')
461
+ continue;
462
+ await walkDir(root, path.join(currentDir, entry.name), excludeDirs, results);
463
+ }
464
+ else if (entry.isFile()) {
465
+ const ext = path.extname(entry.name).toLowerCase();
466
+ if (JAVA_FAMILY_EXTENSIONS.has(ext)) {
467
+ results.push(path.join(currentDir, entry.name));
468
+ }
469
+ }
470
+ }
471
+ }
472
+ // ---------------------------------------------------------------------------
473
+ // Formatting (CLI / MCP consume these)
474
+ // ---------------------------------------------------------------------------
475
+ /**
476
+ * Format the result as agent-friendly markdown. Compact by default to keep
477
+ * token cost low — agent gets signal density without reading whole files.
478
+ */
479
+ function formatCallSitesResult(result, options) {
480
+ const lines = [];
481
+ const receiverWord = result.distinctReceivers === 1 ? 'receiver' : 'receivers';
482
+ lines.push(`**Call sites for \`${result.query}\` (${result.totalFound} found across ${result.distinctReceivers} ${receiverWord}, returning ${result.returned})**`);
483
+ // Group callSites by receiver — they're already clustered + ordered by
484
+ // SNR in searchCallSites: receiver groups sorted by the group's top caller
485
+ // SNR score (xBdy injected-service callers first, noise groups last), and
486
+ // within each group callers are sorted by SNR desc. We just walk and split
487
+ // on receiver change.
488
+ let currentReceiver = undefined;
489
+ for (const site of result.callSites) {
490
+ if (site.receiverText !== currentReceiver) {
491
+ const summary = result.receivers.find((r) => r.receiver === site.receiverText);
492
+ const label = site.receiverText ?? '(unqualified)';
493
+ const totalStr = summary
494
+ ? ` (${summary.total} ${summary.total === 1 ? 'call' : 'calls'}${summary.returned < summary.total ? `, top ${summary.returned}` : ''})`
495
+ : '';
496
+ lines.push('');
497
+ lines.push(`### ${label}.${site.methodName}${totalStr}`);
498
+ currentReceiver = site.receiverText;
499
+ }
500
+ const loc = `${site.filePath}:${site.line}`;
501
+ const ctx = [];
502
+ if (site.containingMethod) {
503
+ ctx.push(site.containingClass ? `${site.containingClass}#${site.containingMethod}` : `#${site.containingMethod}`);
504
+ }
505
+ else if (site.containingClass) {
506
+ ctx.push(site.containingClass);
507
+ }
508
+ const ctxStr = ctx.length > 0 ? ` _in ${ctx.join(' :: ')}_` : '';
509
+ lines.push(`- \`${site.confidence}\` — ${site.reason}`);
510
+ lines.push(` \`${loc}\`${ctxStr}`);
511
+ if (site.snippet) {
512
+ const snippetLines = site.snippet.split('\n').slice(0, 3);
513
+ for (const sl of snippetLines) {
514
+ lines.push(` > ${sl}`);
515
+ }
516
+ }
517
+ if (options?.explain) {
518
+ lines.push(` _score: ${formatSnrBreakdown(site)}_`);
519
+ }
520
+ }
521
+ // Omitted summary — transparency so agent knows what was filtered
522
+ const o = result.omitted;
523
+ const omittedParts = [];
524
+ if (o.skippedFiles > 0)
525
+ omittedParts.push(`${o.skippedFiles} files without text match`);
526
+ if (o.filteredByReceiver > 0)
527
+ omittedParts.push(`${o.filteredByReceiver} filtered by receiver`);
528
+ if (o.truncatedByTopK > 0)
529
+ omittedParts.push(`${o.truncatedByTopK} past topK`);
530
+ if (o.truncatedByBudget > 0)
531
+ omittedParts.push(`${o.truncatedByBudget} truncated by budget`);
532
+ if (omittedParts.length > 0) {
533
+ lines.push('');
534
+ lines.push(`_Omitted: ${omittedParts.join(' · ')}_`);
535
+ }
536
+ // Other receivers truncated by topK — surfaced so the agent knows they
537
+ // exist without re-querying. Tells the agent "if you need calls from these
538
+ // receivers, re-query with --receiver X".
539
+ const unreturnedReceivers = result.receivers.filter((r) => r.returned === 0);
540
+ if (unreturnedReceivers.length > 0) {
541
+ lines.push('');
542
+ const summary = unreturnedReceivers
543
+ .map((r) => `${r.receiver ?? '(unqualified)'} (${r.total})`)
544
+ .join(' · ');
545
+ lines.push(`_Other receivers (topK reached, use \`-r <receiver>\` to drill in): ${summary}_`);
546
+ }
547
+ lines.push('');
548
+ lines.push('_Note: AST-based — comments and string literals are excluded by construction (reported as 0)._');
549
+ return lines.join('\n');
550
+ }
551
+ /**
552
+ * Resolve a symbol reference to its AST method_declaration node.
553
+ *
554
+ * Supported formats:
555
+ * - `file:line` — absolute precision; finds the method_declaration containing that line
556
+ * - `Class#method` — text-search for `class Class`, then AST-verifies the method inside
557
+ * - `pkg.Class#method` — same as above, pkg is currently ignored (text search by Class name only)
558
+ */
559
+ async function resolveMethodSymbol(projectRoot, symbol, languages) {
560
+ // Case 1: file:line
561
+ const lineMatch = symbol.match(/^(.+):(\d+)$/);
562
+ if (lineMatch) {
563
+ const [, relPath, lineStr] = lineMatch;
564
+ if (!relPath || !lineStr) {
565
+ throw new Error(`Invalid file:line symbol: "${symbol}"`);
566
+ }
567
+ const file = path.resolve(projectRoot, relPath);
568
+ const content = await fs_1.promises.readFile(file, 'utf-8');
569
+ const language = (0, grammars_1.detectLanguage)(file, content);
570
+ if (!languages.includes(language)) {
571
+ throw new Error(`File ${relPath} is ${language}, not in supported ${languages.join('/')}`);
572
+ }
573
+ await (0, grammars_1.loadGrammarsForLanguages)([language]);
574
+ const parser = (0, grammars_1.getParser)(language);
575
+ if (!parser)
576
+ throw new Error(`No parser available for ${language}`);
577
+ const tree = parser.parse(content);
578
+ if (!tree)
579
+ throw new Error(`Failed to parse ${relPath}`);
580
+ const line = parseInt(lineStr, 10);
581
+ const methodNode = findMethodDeclarationContaining(tree.rootNode, line);
582
+ if (!methodNode) {
583
+ throw new Error(`No method_declaration contains line ${line} in ${relPath}`);
584
+ }
585
+ return {
586
+ file,
587
+ methodNode,
588
+ methodQualifiedName: qualifyMethod(methodNode),
589
+ };
590
+ }
591
+ // Case 2: Class#method
592
+ const hashMatch = symbol.match(/^(?:([\w.]+)\.)?(\w+)#(\w+)$/);
593
+ if (hashMatch) {
594
+ const [, , className, methodName] = hashMatch;
595
+ if (!className || !methodName) {
596
+ throw new Error(`Invalid Class#method symbol: "${symbol}"`);
597
+ }
598
+ const candidateFiles = await findFilesWithClass(projectRoot, className);
599
+ await (0, grammars_1.loadGrammarsForLanguages)(languages);
600
+ let lastErr = null;
601
+ for (const file of candidateFiles) {
602
+ let content;
603
+ try {
604
+ content = await fs_1.promises.readFile(file, 'utf-8');
605
+ }
606
+ catch {
607
+ continue;
608
+ }
609
+ const language = (0, grammars_1.detectLanguage)(file, content);
610
+ if (!languages.includes(language))
611
+ continue;
612
+ const parser = (0, grammars_1.getParser)(language);
613
+ if (!parser)
614
+ continue;
615
+ const tree = parser.parse(content);
616
+ if (!tree)
617
+ continue;
618
+ const methodNode = findMethodInClass(tree.rootNode, className, methodName);
619
+ if (methodNode) {
620
+ return {
621
+ file,
622
+ methodNode,
623
+ methodQualifiedName: `${className}#${methodName}`,
624
+ };
625
+ }
626
+ lastErr = new Error(`Class ${className} found in ${file} but method ${methodName} not declared`);
627
+ }
628
+ throw lastErr ?? new Error(`Class ${className} not found in any source file`);
629
+ }
630
+ throw new Error(`Unrecognized symbol format: "${symbol}". Use "file:line" (e.g., src/Foo.java:82) or "Class#method" (e.g., Foo#bar).`);
631
+ }
632
+ /**
633
+ * Find the deepest method_declaration whose range contains `line`.
634
+ * Tolerates nested methods (anonymous classes / lambdas) by preferring the inner-most.
635
+ */
636
+ function findMethodDeclarationContaining(root, line) {
637
+ let best = null;
638
+ (0, ast_helpers_1.walkTree)(root, (n) => {
639
+ if (n.type === 'method_declaration' ||
640
+ n.type === 'constructor_declaration' ||
641
+ n.type === 'compact_constructor_declaration') {
642
+ const startLine = n.startPosition.row + 1;
643
+ const endLine = n.endPosition.row + 1;
644
+ if (line >= startLine && line <= endLine) {
645
+ // Deepest = latest-starting (inner-most for nested)
646
+ if (!best || n.startPosition.row > best.startPosition.row) {
647
+ best = n;
648
+ }
649
+ }
650
+ }
651
+ });
652
+ return best;
653
+ }
654
+ /**
655
+ * Find a method_declaration named `methodName` inside any class/interface/enum/record
656
+ * named `className`. Walks all type-declaration kinds so that interface methods
657
+ * (`IFoo#bar`) resolve correctly, not just concrete-class methods.
658
+ */
659
+ function findMethodInClass(root, className, methodName) {
660
+ let found = null;
661
+ (0, ast_helpers_1.walkTree)(root, (n) => {
662
+ if (found)
663
+ return;
664
+ if (n.type !== 'class_declaration' &&
665
+ n.type !== 'interface_declaration' &&
666
+ n.type !== 'enum_declaration' &&
667
+ n.type !== 'record_declaration')
668
+ return;
669
+ const nameNode = n.childForFieldName('name');
670
+ if (!nameNode || nameNode.text !== className)
671
+ return;
672
+ (0, ast_helpers_1.walkTree)(n, (m) => {
673
+ if (found)
674
+ return;
675
+ if (m.type !== 'method_declaration' && m.type !== 'constructor_declaration')
676
+ return;
677
+ const mNameNode = m.childForFieldName('name');
678
+ if (mNameNode && mNameNode.text === methodName) {
679
+ found = m;
680
+ }
681
+ });
682
+ });
683
+ return found;
684
+ }
685
+ function qualifyMethod(methodNode) {
686
+ const classNode = findAncestorName(methodNode, [
687
+ 'class_declaration',
688
+ 'interface_declaration',
689
+ 'enum_declaration',
690
+ 'record_declaration',
691
+ ]);
692
+ const nameNode = methodNode.childForFieldName('name');
693
+ const methodName = nameNode?.text ?? '<anonymous>';
694
+ return classNode ? `${classNode}#${methodName}` : methodName;
695
+ }
696
+ /**
697
+ * Find the nearest enclosing named type declaration (class/interface/enum/record)
698
+ * for a node. Returns the declaration node itself, not just its name.
699
+ */
700
+ function findEnclosingClassType(node) {
701
+ let cur = node.parent;
702
+ while (cur) {
703
+ if (cur.type === 'class_declaration' ||
704
+ cur.type === 'interface_declaration' ||
705
+ cur.type === 'enum_declaration' ||
706
+ cur.type === 'record_declaration') {
707
+ return cur;
708
+ }
709
+ cur = cur.parent;
710
+ }
711
+ return null;
712
+ }
713
+ /**
714
+ * Walk a class body, find every field_declaration whose modifiers carry an
715
+ * injection annotation (@Resource / @Autowired / @Inject), and return a map
716
+ * keyed by field name. Used to mark cross-boundary callees.
717
+ *
718
+ * Java/Kotlin grammar: `field_declaration` has `modifiers` (list of modifier
719
+ * + annotation nodes) and `type`, and contains one or more `variable_declarator`
720
+ * nodes (each with a `name`). Multiple declarators share the same type+annotation.
721
+ *
722
+ * Annotations we recognize (Java):
723
+ * - jakarta/javax.annotation.Resource — JSR-250
724
+ * - org.springframework.beans.factory.annotation.Autowired — Spring
725
+ * - jakarta.inject.Inject — JSR-330
726
+ *
727
+ * Kotlin equivalent (kotlinx injected via spring) uses the same annotation types
728
+ * on `property_declaration`; we don't yet handle Kotlin property syntax, so
729
+ * Kotlin injection is under-detected — fine for the Java-first hx use case.
730
+ *
731
+ * The annotation short-name set lives in ./snr-constants.ts (INJECTION_ANNOTATIONS).
732
+ */
733
+ function extractInjectedFields(classNode) {
734
+ const result = new Map();
735
+ (0, ast_helpers_1.walkTree)(classNode, (n) => {
736
+ if (n.type !== 'field_declaration')
737
+ return;
738
+ // tree-sitter-java: field_declaration has `type` as a named field, but
739
+ // `modifiers` is a positional child node (type 'modifiers'), not a named
740
+ // field — childForFieldName('modifiers') returns null. Iterate children.
741
+ let modifiersNode = null;
742
+ for (const child of n.children) {
743
+ if (child?.type === 'modifiers') {
744
+ modifiersNode = child;
745
+ break;
746
+ }
747
+ }
748
+ if (!modifiersNode)
749
+ return;
750
+ let annotation = null;
751
+ for (const child of modifiersNode.children) {
752
+ if (!child)
753
+ continue;
754
+ if (child.type !== 'annotation' && child.type !== 'marker_annotation')
755
+ continue;
756
+ const nameNode = child.childForFieldName('name');
757
+ if (!nameNode)
758
+ continue;
759
+ // Spring/javax annotations may be fully-qualified in some codebases;
760
+ // take the last segment for the lookup.
761
+ const shortName = nameNode.text.split('.').pop() ?? nameNode.text;
762
+ if (snr_constants_1.INJECTION_ANNOTATIONS.has(shortName)) {
763
+ annotation = shortName;
764
+ break;
765
+ }
766
+ }
767
+ if (!annotation)
768
+ return;
769
+ const typeNode = n.childForFieldName('type');
770
+ if (!typeNode)
771
+ return;
772
+ const fieldType = typeNode.text;
773
+ (0, ast_helpers_1.walkTree)(n, (v) => {
774
+ if (v.type !== 'variable_declarator')
775
+ return;
776
+ const nameNode = v.childForFieldName('name');
777
+ if (!nameNode || nameNode.type !== 'identifier')
778
+ return;
779
+ result.set(nameNode.text, { fieldName: nameNode.text, fieldType, annotation });
780
+ });
781
+ });
782
+ return result;
783
+ }
784
+ /**
785
+ * Find source files that textually look like they declare `class className`.
786
+ * Uses a fast regex pre-filter; AST verification happens later in findMethodInClass.
787
+ */
788
+ async function findFilesWithClass(projectRoot, className) {
789
+ const allFiles = await listJavaFamilyFiles(projectRoot, exports.DEFAULT_EXCLUDE_DIRS);
790
+ const candidates = [];
791
+ const pattern = new RegExp(`\\b(class|interface|enum|record)\\s+${escapeRegex(className)}\\b`);
792
+ for (const file of allFiles) {
793
+ try {
794
+ const content = await fs_1.promises.readFile(file, 'utf-8');
795
+ if (pattern.test(content)) {
796
+ candidates.push(file);
797
+ }
798
+ }
799
+ catch {
800
+ // skip unreadable
801
+ }
802
+ }
803
+ return candidates;
804
+ }
805
+ function escapeRegex(s) {
806
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
807
+ }
808
+ // ===========================================================================
809
+ // SNR scoring (spec §2)
810
+ //
811
+ // Per-callee score that ranks "agent would need to follow this hop" higher
812
+ // than "boilerplate the agent can ignore". Validated against 6 hx samples
813
+ // (see docs/features/2026-07-01-calls-command/snr-probe.md).
814
+ // ===========================================================================
815
+ // RE_BIZ_VERB and the rest of the SNR-scoring regex/Set constants now live
816
+ // in ./snr-constants.ts (shared with context.ts).
817
+ /**
818
+ * Split a camelCase / PascalCase identifier into lowercase segments.
819
+ * `saleOrderSceneGroupCommit` → `['sale', 'order', 'scene', 'group', 'commit']`.
820
+ *
821
+ * Used by same-class biz detection (§2.3) so methods whose biz verb appears
822
+ * as an inner segment — `saleOrderSceneGroupCommit` contains `scene` and
823
+ * `commit` — still get tagged. The strict `^biz-verb` prefix in RE_BIZ_VERB
824
+ * would miss these. Probe sample 3 surfaced exactly this case.
825
+ */
826
+ function camelCaseSegments(name) {
827
+ return name.split(/(?=[A-Z])/).map((s) => s.toLowerCase());
828
+ }
829
+ /** True if any camelCase segment of `name` starts with a biz verb. */
830
+ function containsBizVerbSegment(name) {
831
+ return camelCaseSegments(name).some((s) => snr_constants_1.RE_BIZ_VERB.test(s));
832
+ }
833
+ /**
834
+ * Param / local VO variable names observed across the hx corpus. Matched as
835
+ * exact root identifier (after `rootIdentifier()` extraction). Generalized
836
+ * `param/params/vo/dto/resp/req/result/response/exception/e/ex/err` cover
837
+ * most local-var patterns; hx-specific names kept for fidelity but the
838
+ * heuristic generalizes via the suffix-match in `looksLikeVO`.
839
+ */
840
+ const PARAM_VO_NAMES = new Set([
841
+ 'reqVo', 'rspVo', 'reqVo1', 'rspVo1',
842
+ 'req', 'rsp', 'request', 'response', 'resp',
843
+ 'result', 'finalResult', 'curResult', 'responseVo', 'errRspVo',
844
+ 'baseInfo', 'orderInfo', 'prod', 'ord', 'safe',
845
+ 'custOrder', 'customerOrder', 'sysCustomerOrder',
846
+ 'cartDelReqVo', 'chargeInfo', 'chargeInfoByCustOrderNbr', 'pkgGrpReqVo',
847
+ 'confirmRspVo', 'createCustomerOrderRspVo', 'createCustomerOrderReqVo',
848
+ 'reqReceiptVo', 'acctInfo', 'newAcctInfo', 'respAcctInfo',
849
+ 'exception', 'e', 'ex', 'err', 't',
850
+ 'param', 'params', 'map', 'vo', 'dto',
851
+ ]);
852
+ /**
853
+ * Static-const access pattern: `UpperCamel.UPPER_SNAKE`. Catches `SoMDA.X`,
854
+ * `BizCodeType.X`, `ErrIntfResultCode.X`, `SomeEnum.X`. The all-caps second
855
+ * segment distinguishes from `UpperCamel.lowerMethod(` calls.
856
+ */
857
+ const RE_STATIC_CONST = /^[A-Z][a-zA-Z0-9]*\.[A-Z][A-Z0-9_]+/;
858
+ /**
859
+ * Stream-chain receiver: contains `.stream()` or a `.map(/.filter(/...` call
860
+ * (the opening paren distinguishes from getter `getX`).
861
+ */
862
+ function isStreamChain(receiver) {
863
+ return (receiver.includes('.stream()') ||
864
+ /\.(map|filter|collect|forEach|reduce|flatMap|sorted|distinct|limit|peek)\s*\(/.test(receiver));
865
+ }
866
+ /**
867
+ * Literal receiver: starts (after whitespace/parens) with a quote or digit.
868
+ * Catches `"Y".equals(...)`, `0`, etc.
869
+ */
870
+ function isLiteral(receiver) {
871
+ return /^[\s(]*["'`\d]/.test(receiver);
872
+ }
873
+ /**
874
+ * Extract the root identifier from a (possibly chained/cast) receiver.
875
+ * `((BxError) e)` → `e`; `sysCustomerOrder.getOrderInfo()` → `sysCustomerOrder`;
876
+ * `prods` → `prods`. Returns the LAST identifier in the first dot-segment,
877
+ * which correctly skips cast type names.
878
+ */
879
+ function rootIdentifier(receiver) {
880
+ const firstPart = receiver.split('.')[0] ?? '';
881
+ const idents = firstPart.match(/[a-zA-Z_$][a-zA-Z0-9_$]*/g);
882
+ if (!idents || idents.length === 0)
883
+ return '';
884
+ return idents[idents.length - 1] ?? '';
885
+ }
886
+ /**
887
+ * Compute the SNR score for a callee (spec §2).
888
+ *
889
+ * Receiver features are mutually exclusive and applied first; method-name
890
+ * features apply on top UNLESS the receiver is xBdy (short-circuit per §2.1).
891
+ *
892
+ * xBdy callees short-circuit to +100 — every other consideration is noise
893
+ * relative to "this is an injected business collaborator".
894
+ */
895
+ function computeSnrScore(site) {
896
+ return computeSnrBreakdown(site).total;
897
+ }
898
+ function computeSnrBreakdown(site) {
899
+ // xBdy short-circuit
900
+ if (site.crossBoundary) {
901
+ return { receiver: 100, method: 0, total: 100, matchedRules: ['xBdy'] };
902
+ }
903
+ let receiverScore = 0;
904
+ let methodScore = 0;
905
+ const matchedRules = [];
906
+ const receiver = site.receiverText ?? '';
907
+ const method = site.methodName;
908
+ // === Receiver features (mutually exclusive) ===
909
+ if (site.sameClassBiz) {
910
+ receiverScore += 50; // sibling biz-verb in same class
911
+ matchedRules.push('sibling');
912
+ }
913
+ else if (receiver === '') {
914
+ receiverScore += 10; // unqualified, not biz — likely this.helper() or static import
915
+ matchedRules.push('unqualified');
916
+ }
917
+ else if (isStreamChain(receiver)) {
918
+ receiverScore -= 60;
919
+ matchedRules.push('streamOp');
920
+ }
921
+ else if (isLiteral(receiver)) {
922
+ receiverScore -= 40;
923
+ matchedRules.push('literal');
924
+ }
925
+ else {
926
+ const root = rootIdentifier(receiver);
927
+ if (snr_constants_1.RE_JDK_STDLIB.test(root)) {
928
+ receiverScore -= 60;
929
+ matchedRules.push('jdkStdlib');
930
+ }
931
+ else if (snr_constants_1.RE_APACHE_UTIL.test(root)) {
932
+ receiverScore -= 60;
933
+ matchedRules.push('apacheUtil');
934
+ }
935
+ else if (snr_constants_1.RE_GUAVA.test(root)) {
936
+ receiverScore -= 60;
937
+ matchedRules.push('guavaUtil');
938
+ }
939
+ else if (root === 'log' || /Logger$/.test(root)) {
940
+ receiverScore -= 60;
941
+ matchedRules.push('logCall');
942
+ }
943
+ else if (PARAM_VO_NAMES.has(root)) {
944
+ receiverScore -= 30;
945
+ matchedRules.push('paramVo');
946
+ }
947
+ else if (RE_STATIC_CONST.test(receiver)) {
948
+ receiverScore -= 40;
949
+ matchedRules.push('staticConst');
950
+ }
951
+ else if (/Util$|Helper$/.test(root)) {
952
+ receiverScore -= 50;
953
+ matchedRules.push('utilClass');
954
+ }
955
+ else if (/(Service|Client|Api|Mapper|Repository|Facade|Gateway|Busi)$/i.test(root)) {
956
+ receiverScore += 60;
957
+ matchedRules.push('serviceIsh');
958
+ }
959
+ // else: neutral (score += 0)
960
+ }
961
+ // === Method-name features (apply to all non-xBdy, including sameClassBiz) ===
962
+ // sameClassBiz implies biz-verb (per §2.3), so the biz-verb branch naturally
963
+ // adds +30 on top of the +50 from sameClassBiz — sameClassBiz callee totals +80.
964
+ if (snr_constants_1.OBJECT_METHODS.has(method)) {
965
+ methodScore -= 80;
966
+ matchedRules.push('objectMethod');
967
+ }
968
+ else if (snr_constants_1.MAP_OPS.has(method)) {
969
+ methodScore -= 25;
970
+ matchedRules.push('mapOp');
971
+ }
972
+ else if (snr_constants_1.RE_GETTER.test(method)) {
973
+ methodScore -= 20;
974
+ matchedRules.push('getter');
975
+ }
976
+ else if (snr_constants_1.RE_SETTER.test(method)) {
977
+ methodScore -= 20;
978
+ matchedRules.push('setter');
979
+ }
980
+ else if (snr_constants_1.RE_BOOL_GETTER.test(method)) {
981
+ methodScore -= 15;
982
+ matchedRules.push('boolGetter');
983
+ }
984
+ else if (snr_constants_1.RE_BIZ_VERB.test(method)) {
985
+ methodScore += 30;
986
+ matchedRules.push('bizVerb');
987
+ }
988
+ return {
989
+ receiver: receiverScore,
990
+ method: methodScore,
991
+ total: receiverScore + methodScore,
992
+ matchedRules,
993
+ };
994
+ }
995
+ /**
996
+ * Format an SNR breakdown as a single-line score explanation for the
997
+ * `--explain` flag. Shape:
998
+ * `+100 xBdy = 100`
999
+ * `+60 serviceIsh +30 bizVerb = 90`
1000
+ * `-60 jdkStdlib -80 objectMethod = -140`
1001
+ * `+10 unqualified -25 mapOp = -15`
1002
+ *
1003
+ * Neutral axes (no rule fired) are omitted to keep the breakdown scannable.
1004
+ *
1005
+ * Only fires on non-impact call sites — impact call sites have hop decay and
1006
+ * use `formatImpactSnrBreakdown` instead.
1007
+ */
1008
+ function formatSnrBreakdown(site) {
1009
+ const bd = computeSnrBreakdown(site);
1010
+ const receiverRules = bd.matchedRules.filter(isReceiverRule);
1011
+ const methodRules = bd.matchedRules.filter(isMethodRule);
1012
+ const parts = [];
1013
+ // xBdy short-circuits, so receiver is the only axis that matters.
1014
+ if (receiverRules.length > 0) {
1015
+ parts.push(formatScoreComponent(bd.receiver, receiverRules));
1016
+ }
1017
+ if (methodRules.length > 0) {
1018
+ parts.push(formatScoreComponent(bd.method, methodRules));
1019
+ }
1020
+ // Edge case: both axes neutral (score 0, no rules) — show `+0` so the
1021
+ // line isn't empty.
1022
+ if (parts.length === 0) {
1023
+ parts.push('+0');
1024
+ }
1025
+ return `${parts.join(' ')} = ${bd.total}`;
1026
+ }
1027
+ /**
1028
+ * Format a score component as `[+|-]<points> <rule1,rule2>`.
1029
+ */
1030
+ function formatScoreComponent(score, rules) {
1031
+ const sign = score >= 0 ? '+' : '';
1032
+ const rulesStr = rules.length > 0 ? ` ${rules.join(',')}` : '';
1033
+ return `${sign}${score}${rulesStr}`;
1034
+ }
1035
+ /**
1036
+ * Receiver-axis rules — those applied first (mutually exclusive). Used to
1037
+ * split the breakdown into receiver / method halves for display.
1038
+ */
1039
+ const RECEIVER_RULES = new Set([
1040
+ 'xBdy',
1041
+ 'sibling',
1042
+ 'unqualified',
1043
+ 'streamOp',
1044
+ 'literal',
1045
+ 'jdkStdlib',
1046
+ 'apacheUtil',
1047
+ 'guavaUtil',
1048
+ 'logCall',
1049
+ 'paramVo',
1050
+ 'staticConst',
1051
+ 'utilClass',
1052
+ 'serviceIsh',
1053
+ ]);
1054
+ function isReceiverRule(rule) {
1055
+ return RECEIVER_RULES.has(rule);
1056
+ }
1057
+ function isMethodRule(rule) {
1058
+ return !RECEIVER_RULES.has(rule);
1059
+ }
1060
+ /**
1061
+ * Compare two callees by SNR score desc, then by filePath depth asc (shallower
1062
+ * = more central), then by line asc (call order). Stable tiebreaker chain.
1063
+ */
1064
+ function compareBySnr(a, b) {
1065
+ const sa = a.snrScore ?? 0;
1066
+ const sb = b.snrScore ?? 0;
1067
+ if (sa !== sb)
1068
+ return sb - sa;
1069
+ const da = a.filePath.split('/').length;
1070
+ const db = b.filePath.split('/').length;
1071
+ if (da !== db)
1072
+ return da - db;
1073
+ if (a.filePath !== b.filePath)
1074
+ return a.filePath.localeCompare(b.filePath);
1075
+ return a.line - b.line;
1076
+ }
1077
+ /**
1078
+ * Collect names of all method/constructor declarations directly in a class body.
1079
+ * Used by same-class biz-verb detection (§2.3) — when an unqualified call's
1080
+ * method name is in this set AND matches a biz-verb regex, mark the callee as
1081
+ * `sameClassBiz: true`.
1082
+ *
1083
+ * Walks only direct children of the class_body (no nested-class descent) so
1084
+ * that a method declared in an inner class isn't mistaken for a sibling of
1085
+ * the outer class's methods.
1086
+ */
1087
+ function collectSameClassMethodNames(classNode) {
1088
+ const names = new Set();
1089
+ // Find the class_body child of the class_declaration.
1090
+ let body = null;
1091
+ for (const child of classNode.children) {
1092
+ if (child?.type === 'class_body' || child?.type === 'enum_body' || child?.type === 'interface_body' || child?.type === 'record_body') {
1093
+ body = child;
1094
+ break;
1095
+ }
1096
+ }
1097
+ if (!body)
1098
+ return names;
1099
+ // Direct children only (no descent) — avoids catching inner-class methods.
1100
+ for (const child of body.children) {
1101
+ if (!child)
1102
+ continue;
1103
+ if (child.type === 'method_declaration' ||
1104
+ child.type === 'constructor_declaration' ||
1105
+ child.type === 'compact_constructor_declaration') {
1106
+ const nameNode = child.childForFieldName('name');
1107
+ if (nameNode)
1108
+ names.add(nameNode.text);
1109
+ }
1110
+ }
1111
+ return names;
1112
+ }
1113
+ /**
1114
+ * Mark `sameClassBiz: true` on each callee that satisfies §2.3:
1115
+ * - receiverText is null (unqualified call)
1116
+ * - methodName contains a biz-verb segment (camelCase-aware — catches
1117
+ * `saleOrderSceneGroupCommit`, which the strict prefix regex misses)
1118
+ * - methodName is declared in the same class
1119
+ *
1120
+ * Returns a new array (does not mutate input). Exported for unit tests;
1121
+ * production path inlines the same logic in `searchCallees` to avoid the
1122
+ * extra array copy.
1123
+ */
1124
+ function markSameClassBiz(callees, siblingNames) {
1125
+ return callees.map((c) => {
1126
+ if ((c.receiverText === null || c.receiverText === '') &&
1127
+ containsBizVerbSegment(c.methodName) &&
1128
+ siblingNames.has(c.methodName)) {
1129
+ return { ...c, sameClassBiz: true };
1130
+ }
1131
+ return c;
1132
+ });
1133
+ }
1134
+ /**
1135
+ * Find callees of a method — what does it call internally?
1136
+ * Walks the method body and collects every method_invocation, clustered by receiver.
1137
+ *
1138
+ * Ranking (spec §2):
1139
+ * 1. crossBoundary (`@Resource`/`@Autowired`/`@Inject`) — SNR +100
1140
+ * 2. sameClassBiz (unqualified + biz-verb + declared in same class) — SNR +50
1141
+ * 3. service-ish receiver name — SNR +60
1142
+ * 4. JDK/Apache stdlib, stream chains, param/local VO — negative scores
1143
+ * 5. method-name patterns stacked on top (except for xBdy which short-circuits)
1144
+ *
1145
+ * Cross-boundary detection walks the enclosing class for injected field
1146
+ * declarations; same-class biz detection walks it for sibling method names.
1147
+ * Both feed into `computeSnrScore`, which is what the sort uses.
1148
+ *
1149
+ * Tiered budget (spec §3): essential tier always emitted for every kept
1150
+ * callee; recommended/optional tiers filled in SNR order while budget
1151
+ * remains. `withSnippet` forces minimum recommended tier; `full` forces
1152
+ * optional tier and ignores the budget ceiling.
1153
+ */
1154
+ async function searchCallees(projectRoot, symbol, options = {}) {
1155
+ const { topK = 20, topPerReceiver = 3, budget = 4000, languages = ['java', 'kotlin'], withSnippet = false, full = false, } = options;
1156
+ // 1. Resolve symbol → method_declaration AST node
1157
+ const resolved = await resolveMethodSymbol(projectRoot, symbol, languages);
1158
+ const content = await fs_1.promises.readFile(resolved.file, 'utf-8');
1159
+ // 2. Walk the method body, collect ALL method_invocation (wildcard)
1160
+ const rawCallees = extractMethodInvocations(resolved.methodNode, content, null, // null = wildcard (collect every method_invocation in the body)
1161
+ projectRoot, resolved.file);
1162
+ // 3. Cross-boundary + same-class biz detection
1163
+ const classNode = findEnclosingClassType(resolved.methodNode);
1164
+ const injectedFields = classNode
1165
+ ? extractInjectedFields(classNode)
1166
+ : new Map();
1167
+ const siblingNames = classNode ? collectSameClassMethodNames(classNode) : new Set();
1168
+ const allCallees = rawCallees.map((site) => {
1169
+ // xBdy
1170
+ if (site.receiverText) {
1171
+ const injected = injectedFields.get(site.receiverText);
1172
+ if (injected) {
1173
+ site.crossBoundary = {
1174
+ kind: 'injected',
1175
+ fieldType: injected.fieldType,
1176
+ annotation: injected.annotation,
1177
+ };
1178
+ }
1179
+ }
1180
+ // sameClassBiz (only if NOT xBdy — xBdy short-circuits in computeSnrScore anyway,
1181
+ // but we also don't want to falsely tag an xBdy field call as sameClassBiz).
1182
+ if (!site.crossBoundary &&
1183
+ (site.receiverText === null || site.receiverText === '') &&
1184
+ containsBizVerbSegment(site.methodName) &&
1185
+ siblingNames.has(site.methodName)) {
1186
+ site.sameClassBiz = true;
1187
+ }
1188
+ // SNR score
1189
+ site.snrScore = computeSnrScore(site);
1190
+ return site;
1191
+ });
1192
+ // 4. Cluster by receiver
1193
+ const distinctReceivers = new Set(allCallees.map((s) => s.receiverText)).size;
1194
+ const byReceiver = new Map();
1195
+ for (const site of allCallees) {
1196
+ const key = site.receiverText;
1197
+ const group = byReceiver.get(key) ?? [];
1198
+ group.push(site);
1199
+ byReceiver.set(key, group);
1200
+ }
1201
+ // Within each receiver group, sort by SNR desc (was: line asc)
1202
+ for (const group of byReceiver.values()) {
1203
+ group.sort(compareBySnr);
1204
+ }
1205
+ // Receivers ordered by their top callee's SNR score (was: xBdy first, then size).
1206
+ // xBdy callees score +100 so xBdy groups naturally rise to the top; sameClassBiz
1207
+ // groups (+80) come next; high-SNR non-xBdy groups follow; pure-noise groups
1208
+ // (e.g., CollectionUtils) sort to the bottom.
1209
+ const sortedReceivers = [...byReceiver.entries()].sort((a, b) => {
1210
+ const aTop = a[1][0]?.snrScore ?? -Infinity;
1211
+ const bTop = b[1][0]?.snrScore ?? -Infinity;
1212
+ if (aTop !== bTop)
1213
+ return bTop - aTop;
1214
+ return b[1].length - a[1].length; // tiebreak: larger group first
1215
+ });
1216
+ const receivers = [];
1217
+ const topSites = [];
1218
+ for (const [receiverKey, group] of sortedReceivers) {
1219
+ const injected = receiverKey ? injectedFields.get(receiverKey) : undefined;
1220
+ const topSnr = group[0]?.snrScore;
1221
+ const hasSibling = group.some((c) => c.sameClassBiz === true);
1222
+ if (topSites.length >= topK) {
1223
+ receivers.push({
1224
+ receiver: receiverKey,
1225
+ total: group.length,
1226
+ returned: 0,
1227
+ crossBoundary: injected,
1228
+ sameClassBiz: hasSibling || undefined,
1229
+ topSnrScore: topSnr,
1230
+ });
1231
+ continue;
1232
+ }
1233
+ const take = Math.min(topPerReceiver, group.length, topK - topSites.length);
1234
+ topSites.push(...group.slice(0, take));
1235
+ receivers.push({
1236
+ receiver: receiverKey,
1237
+ total: group.length,
1238
+ returned: take,
1239
+ crossBoundary: injected,
1240
+ sameClassBiz: hasSibling || undefined,
1241
+ topSnrScore: topSnr,
1242
+ });
1243
+ }
1244
+ const truncatedByTopK = Math.max(0, allCallees.length - topSites.length);
1245
+ // 5. Sort the global topSites by SNR desc (so tiered-budget pass fills in order)
1246
+ topSites.sort(compareBySnr);
1247
+ // 6. Apply tiered budget (spec §3)
1248
+ const { tiered, truncatedByBudget } = applyTieredBudget(topSites, budget, {
1249
+ forceRecommended: withSnippet,
1250
+ forceOptional: full,
1251
+ });
1252
+ return {
1253
+ symbol,
1254
+ methodQualifiedName: resolved.methodQualifiedName,
1255
+ filePath: path.relative(projectRoot, resolved.file).replace(/\\/g, '/'),
1256
+ startLine: resolved.methodNode.startPosition.row + 1,
1257
+ endLine: resolved.methodNode.endPosition.row + 1,
1258
+ totalFound: allCallees.length,
1259
+ returned: tiered.length,
1260
+ distinctReceivers,
1261
+ receivers,
1262
+ injectedFields: [...injectedFields.values()],
1263
+ omitted: {
1264
+ truncatedByTopK,
1265
+ truncatedByBudget,
1266
+ },
1267
+ callees: tiered,
1268
+ };
1269
+ }
1270
+ // ===========================================================================
1271
+ // Tiered budget (spec §3)
1272
+ //
1273
+ // Three-layer output allocation: essential → recommended → optional.
1274
+ // Pass 1 emits essential for every kept callee; pass 2 upgrades callees to
1275
+ // recommended in SNR order while budget remains; pass 3 upgrades to optional.
1276
+ // `forceRecommended` (the --snippet alias) and `forceOptional` (--full) override
1277
+ // the upgrade passes uniformly.
1278
+ // ===========================================================================
1279
+ /** Per-tier marginal char cost (spec §3.3). Ratio ~1:3:10 is locked. */
1280
+ const TIER_ESSENTIAL_COST = 40;
1281
+ const TIER_RECOMMENDED_ADDITIONAL = 120;
1282
+ const TIER_OPTIONAL_ADDITIONAL = 400;
1283
+ /**
1284
+ * Assign each callee a `tier` ('essential' | 'recommended' | 'optional')
1285
+ * according to spec §3.2.
1286
+ *
1287
+ * `sorted` MUST be pre-sorted by SNR desc (use compareBySnr).
1288
+ */
1289
+ function applyTieredBudget(sorted, budget, opts = {}) {
1290
+ // Pass 1: how many callees fit at essential tier?
1291
+ let count = 0;
1292
+ let essentialBudgetUsed = 0;
1293
+ for (let i = 0; i < sorted.length; i++) {
1294
+ if (essentialBudgetUsed + TIER_ESSENTIAL_COST > budget && count > 0)
1295
+ break;
1296
+ essentialBudgetUsed += TIER_ESSENTIAL_COST;
1297
+ count++;
1298
+ }
1299
+ const truncatedByBudget = sorted.length - count;
1300
+ const tiered = sorted.slice(0, count).map((c) => ({ ...c, tier: 'essential' }));
1301
+ // Force flags short-circuit further tier work
1302
+ if (opts.forceOptional) {
1303
+ for (const c of tiered)
1304
+ c.tier = 'optional';
1305
+ return { tiered, truncatedByBudget };
1306
+ }
1307
+ if (opts.forceRecommended) {
1308
+ for (const c of tiered)
1309
+ c.tier = 'recommended';
1310
+ return { tiered, truncatedByBudget };
1311
+ }
1312
+ // Pass 2: upgrade to recommended in SNR order
1313
+ let remaining = budget - essentialBudgetUsed;
1314
+ for (const c of tiered) {
1315
+ if (remaining >= TIER_RECOMMENDED_ADDITIONAL) {
1316
+ c.tier = 'recommended';
1317
+ remaining -= TIER_RECOMMENDED_ADDITIONAL;
1318
+ }
1319
+ }
1320
+ // Pass 3: upgrade recommended → optional in SNR order
1321
+ for (const c of tiered) {
1322
+ if (c.tier !== 'recommended')
1323
+ continue;
1324
+ if (remaining >= TIER_OPTIONAL_ADDITIONAL) {
1325
+ c.tier = 'optional';
1326
+ remaining -= TIER_OPTIONAL_ADDITIONAL;
1327
+ }
1328
+ }
1329
+ return { tiered, truncatedByBudget };
1330
+ }
1331
+ /**
1332
+ * Format callees result as agent-friendly markdown.
1333
+ *
1334
+ * Three-tier output per spec §3.1:
1335
+ * essential — one line per call (always emitted)
1336
+ * recommended — + 1–2 line snippet + enclosing Class#method
1337
+ * optional — same shape as recommended in this MVP (deeper snippet
1338
+ * window / imports / field decls deferred — agent can
1339
+ * `Read file:line` if it needs that depth)
1340
+ *
1341
+ * Per-callee `tier` is set by `applyTieredBudget` (or forced via the
1342
+ * `withSnippet`/`full` opts on `searchCallees`). Receivers are listed in
1343
+ * SNR order — groups whose top callee scores higher come first, so
1344
+ * xBdy/sibling groups surface at the top and pure-noise groups
1345
+ * (CollectionUtils, Objects) sink to the bottom even when they have many
1346
+ * calls.
1347
+ *
1348
+ * Empty callees (spec §5): methods with 0 internal calls get a friendly
1349
+ * message instead of "0 across 0 receivers, returning 0" (which reads as
1350
+ * an error).
1351
+ */
1352
+ function formatCalleesResult(result, options) {
1353
+ const lines = [];
1354
+ lines.push(`**Callees of \`${result.methodQualifiedName}\`** \`${result.filePath}:${result.startLine}-${result.endLine}\``);
1355
+ // Empty-callees UX (spec §5)
1356
+ if (result.totalFound === 0 || result.callees.length === 0) {
1357
+ const methodName = result.methodQualifiedName.split('#')[1] ?? '<method>';
1358
+ lines.push('');
1359
+ lines.push(`_Method has no internal calls — likely returns a constant, field, or a single delegate._`);
1360
+ lines.push(`_Try \`cgraphx calls ${methodName}\` to find callers, or \`Read ${result.filePath}:${result.startLine}\` to see the body._`);
1361
+ return lines.join('\n');
1362
+ }
1363
+ lines.push(`— ${result.totalFound} internal call${result.totalFound === 1 ? '' : 's'} across ${result.distinctReceivers} receiver${result.distinctReceivers === 1 ? '' : 's'}, returning ${result.returned}`);
1364
+ // Iterate receivers in SNR order (searchCallees already sorted them by topSnrScore).
1365
+ // For each receiver with `returned > 0`, print group header + its callees in SNR order.
1366
+ for (const recvSummary of result.receivers) {
1367
+ if (recvSummary.returned === 0)
1368
+ continue;
1369
+ const recvCallees = result.callees.filter((c) => c.receiverText === recvSummary.receiver);
1370
+ if (recvCallees.length === 0)
1371
+ continue;
1372
+ const label = recvSummary.receiver ?? '(unqualified)';
1373
+ const totalStr = ` (${recvSummary.total} ${recvSummary.total === 1 ? 'call' : 'calls'}${recvSummary.returned < recvSummary.total ? `, top ${recvSummary.returned}` : ''})`;
1374
+ const tagStr = recvSummary.crossBoundary
1375
+ ? ` — \`${recvSummary.crossBoundary.fieldType}\` @${recvSummary.crossBoundary.annotation} **[xBdy]**`
1376
+ : recvSummary.sameClassBiz
1377
+ ? ` **[sibling]**`
1378
+ : '';
1379
+ lines.push('');
1380
+ lines.push(`### ${label}${totalStr}${tagStr}`);
1381
+ for (const c of recvCallees) {
1382
+ // Essential line — always emitted
1383
+ lines.push(`- **${c.methodName}** L${c.line}`);
1384
+ // Recommended / optional tier — snippet + enclosing context
1385
+ if (c.tier === 'recommended' || c.tier === 'optional') {
1386
+ if (c.snippet) {
1387
+ const snippetLines = c.snippet.split('\n').slice(0, 2);
1388
+ for (const sl of snippetLines) {
1389
+ lines.push(` > ${sl}`);
1390
+ }
1391
+ }
1392
+ if (c.containingClass && c.containingMethod) {
1393
+ lines.push(` _in ${c.containingClass}#${c.containingMethod}_`);
1394
+ }
1395
+ else if (c.containingMethod) {
1396
+ lines.push(` _in #${c.containingMethod}_`);
1397
+ }
1398
+ }
1399
+ if (options?.explain) {
1400
+ lines.push(` _score: ${formatSnrBreakdown(c)}_`);
1401
+ }
1402
+ }
1403
+ }
1404
+ // Omitted summary
1405
+ const o = result.omitted;
1406
+ const omittedParts = [];
1407
+ if (o.truncatedByTopK > 0)
1408
+ omittedParts.push(`${o.truncatedByTopK} past topK`);
1409
+ if (o.truncatedByBudget > 0)
1410
+ omittedParts.push(`${o.truncatedByBudget} truncated by budget`);
1411
+ if (omittedParts.length > 0) {
1412
+ lines.push('');
1413
+ lines.push(`_Omitted: ${omittedParts.join(' · ')}_`);
1414
+ }
1415
+ // Receivers truncated by topK — surfaced so agent knows they exist
1416
+ const unreturnedReceivers = result.receivers.filter((r) => r.returned === 0);
1417
+ if (unreturnedReceivers.length > 0) {
1418
+ lines.push('');
1419
+ const summary = unreturnedReceivers
1420
+ .map((r) => {
1421
+ const tag = r.crossBoundary
1422
+ ? ` [xBdy:${r.crossBoundary.fieldType}]`
1423
+ : r.sameClassBiz
1424
+ ? ' [sibling]'
1425
+ : '';
1426
+ return `${r.receiver ?? '(unqualified)'} (${r.total})${tag}`;
1427
+ })
1428
+ .join(' · ');
1429
+ lines.push(`_Other receivers (topK reached): ${summary}_`);
1430
+ }
1431
+ return lines.join('\n');
1432
+ }
1433
+ //# sourceMappingURL=call-sites.js.map