@colbymchenry/codegraph-darwin-x64 0.9.8 → 0.9.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/dist/bin/codegraph.js +1 -36
- package/lib/dist/bin/codegraph.js.map +1 -1
- package/lib/dist/context/index.d.ts +9 -0
- package/lib/dist/context/index.d.ts.map +1 -1
- package/lib/dist/context/index.js +95 -6
- package/lib/dist/context/index.js.map +1 -1
- package/lib/dist/context/markers.d.ts +19 -0
- package/lib/dist/context/markers.d.ts.map +1 -0
- package/lib/dist/context/markers.js +22 -0
- package/lib/dist/context/markers.js.map +1 -0
- package/lib/dist/extraction/tree-sitter.d.ts +26 -0
- package/lib/dist/extraction/tree-sitter.d.ts.map +1 -1
- package/lib/dist/extraction/tree-sitter.js +139 -19
- package/lib/dist/extraction/tree-sitter.js.map +1 -1
- package/lib/dist/index.d.ts +7 -1
- package/lib/dist/index.d.ts.map +1 -1
- package/lib/dist/index.js +9 -1
- package/lib/dist/index.js.map +1 -1
- package/lib/dist/installer/targets/shared.d.ts.map +1 -1
- package/lib/dist/installer/targets/shared.js +3 -2
- package/lib/dist/installer/targets/shared.js.map +1 -1
- package/lib/dist/mcp/server-instructions.d.ts +1 -1
- package/lib/dist/mcp/server-instructions.d.ts.map +1 -1
- package/lib/dist/mcp/server-instructions.js +18 -19
- package/lib/dist/mcp/server-instructions.js.map +1 -1
- package/lib/dist/mcp/tools.d.ts +41 -51
- package/lib/dist/mcp/tools.d.ts.map +1 -1
- package/lib/dist/mcp/tools.js +534 -902
- package/lib/dist/mcp/tools.js.map +1 -1
- package/lib/dist/search/query-utils.d.ts +18 -0
- package/lib/dist/search/query-utils.d.ts.map +1 -1
- package/lib/dist/search/query-utils.js +30 -0
- package/lib/dist/search/query-utils.js.map +1 -1
- package/lib/dist/types.d.ts +8 -0
- package/lib/dist/types.d.ts.map +1 -1
- package/lib/node_modules/.package-lock.json +1 -1
- package/lib/package.json +1 -1
- package/package.json +1 -1
package/lib/dist/mcp/tools.js
CHANGED
|
@@ -4,39 +4,6 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Defines the tools exposed by the CodeGraph MCP server.
|
|
6
6
|
*/
|
|
7
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
8
|
-
if (k2 === undefined) k2 = k;
|
|
9
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
10
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
11
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
12
|
-
}
|
|
13
|
-
Object.defineProperty(o, k2, desc);
|
|
14
|
-
}) : (function(o, m, k, k2) {
|
|
15
|
-
if (k2 === undefined) k2 = k;
|
|
16
|
-
o[k2] = m[k];
|
|
17
|
-
}));
|
|
18
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
19
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
20
|
-
}) : function(o, v) {
|
|
21
|
-
o["default"] = v;
|
|
22
|
-
});
|
|
23
|
-
var __importStar = (this && this.__importStar) || (function () {
|
|
24
|
-
var ownKeys = function(o) {
|
|
25
|
-
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
26
|
-
var ar = [];
|
|
27
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
28
|
-
return ar;
|
|
29
|
-
};
|
|
30
|
-
return ownKeys(o);
|
|
31
|
-
};
|
|
32
|
-
return function (mod) {
|
|
33
|
-
if (mod && mod.__esModule) return mod;
|
|
34
|
-
var result = {};
|
|
35
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
36
|
-
__setModuleDefault(result, mod);
|
|
37
|
-
return result;
|
|
38
|
-
};
|
|
39
|
-
})();
|
|
40
7
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
41
8
|
exports.ToolHandler = exports.tools = void 0;
|
|
42
9
|
exports.getExploreBudget = getExploreBudget;
|
|
@@ -52,12 +19,10 @@ const directory_1 = require("../directory");
|
|
|
52
19
|
// sync + cached (CommonJS build).
|
|
53
20
|
const loadCodeGraph = () => require('../index').default;
|
|
54
21
|
const worktree_1 = require("../sync/worktree");
|
|
55
|
-
const
|
|
22
|
+
const query_utils_1 = require("../search/query-utils");
|
|
56
23
|
const fs_1 = require("fs");
|
|
57
24
|
const utils_1 = require("../utils");
|
|
58
25
|
const generated_detection_1 = require("../extraction/generated-detection");
|
|
59
|
-
const os_1 = require("os");
|
|
60
|
-
const pathModule = __importStar(require("path"));
|
|
61
26
|
const path_1 = require("path");
|
|
62
27
|
/** Maximum output length to prevent context bloat (characters) */
|
|
63
28
|
const MAX_OUTPUT_LENGTH = 15000;
|
|
@@ -114,13 +79,24 @@ function getExploreBudget(fileCount) {
|
|
|
114
79
|
return 5;
|
|
115
80
|
}
|
|
116
81
|
function getExploreOutputBudget(fileCount) {
|
|
82
|
+
// Tiered budget, scaled to project size. The budget is a CEILING (relevance
|
|
83
|
+
// still gates WHAT is included), and it MUST stay under the agent's INLINE
|
|
84
|
+
// tool-result cap (~25K chars). Above that, the host externalizes the result
|
|
85
|
+
// to a file the agent then Reads back — re-introducing a read AND the
|
|
86
|
+
// cache-write cost — which is exactly what a 35K vscode explore did in the
|
|
87
|
+
// n=4 README A/B. So even large repos cap at ~24K: the answer is the handful
|
|
88
|
+
// of ~100-line flow windows the agent would have grep-located and read (it
|
|
89
|
+
// natively reads ~6–9 files, median 100-line ranges), NOT a sprawl of 12
|
|
90
|
+
// files. Concentration onto the flow emerges from this cap + the named-file-
|
|
91
|
+
// first sort dropping peripheral files. Invariant: a larger tier must never
|
|
92
|
+
// get a smaller `maxCharsPerFile` than a smaller tier.
|
|
117
93
|
if (fileCount < 150) {
|
|
118
94
|
return {
|
|
119
95
|
// ITER3: revert iter2's aggressive body shrink (forced Read fallback —
|
|
120
96
|
// the per-file 2.5K cap pushed the agent to Read instead of node).
|
|
121
97
|
// Back to the iter1 shape (13K/4/3.8K) but keep the test-file
|
|
122
|
-
// hard-exclude. The cost lever for this tier lives in
|
|
123
|
-
//
|
|
98
|
+
// hard-exclude. The cost lever for this tier lives in steering the
|
|
99
|
+
// agent to stop after 1-2 calls, not in this budget.
|
|
124
100
|
maxOutputChars: 13000,
|
|
125
101
|
defaultMaxFiles: 4,
|
|
126
102
|
maxCharsPerFile: 3800,
|
|
@@ -152,13 +128,11 @@ function getExploreOutputBudget(fileCount) {
|
|
|
152
128
|
}
|
|
153
129
|
if (fileCount < 5000) {
|
|
154
130
|
return {
|
|
155
|
-
//
|
|
156
|
-
//
|
|
157
|
-
//
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
maxOutputChars: 28000,
|
|
161
|
-
defaultMaxFiles: 10,
|
|
131
|
+
// ~150-line per-file window (the native read unit) × ~6 files, capped at
|
|
132
|
+
// the ~24K inline ceiling so the response is never externalized. Per-file
|
|
133
|
+
// stays ≥ the <500 tier (3800) — monotonic.
|
|
134
|
+
maxOutputChars: 24000,
|
|
135
|
+
defaultMaxFiles: 8,
|
|
162
136
|
maxCharsPerFile: 6500,
|
|
163
137
|
gapThreshold: 12,
|
|
164
138
|
maxSymbolsInFileHeader: 10,
|
|
@@ -170,10 +144,14 @@ function getExploreOutputBudget(fileCount) {
|
|
|
170
144
|
excludeLowValueFiles: false,
|
|
171
145
|
};
|
|
172
146
|
}
|
|
147
|
+
// Large + very-large repos: SAME ~24K inline ceiling (a bigger response just
|
|
148
|
+
// externalizes — see vscode). More files indexed → more CALLS via
|
|
149
|
+
// getExploreBudget, not a bigger single response. Per-file 7000 (≥ smaller
|
|
150
|
+
// tiers) gives the central file a ~180-line orientation window.
|
|
173
151
|
if (fileCount < 15000) {
|
|
174
152
|
return {
|
|
175
|
-
maxOutputChars:
|
|
176
|
-
defaultMaxFiles:
|
|
153
|
+
maxOutputChars: 24000,
|
|
154
|
+
defaultMaxFiles: 8,
|
|
177
155
|
maxCharsPerFile: 7000,
|
|
178
156
|
gapThreshold: 15,
|
|
179
157
|
maxSymbolsInFileHeader: 15,
|
|
@@ -186,8 +164,8 @@ function getExploreOutputBudget(fileCount) {
|
|
|
186
164
|
};
|
|
187
165
|
}
|
|
188
166
|
return {
|
|
189
|
-
maxOutputChars:
|
|
190
|
-
defaultMaxFiles:
|
|
167
|
+
maxOutputChars: 24000,
|
|
168
|
+
defaultMaxFiles: 8,
|
|
191
169
|
maxCharsPerFile: 7000,
|
|
192
170
|
gapThreshold: 15,
|
|
193
171
|
maxSymbolsInFileHeader: 15,
|
|
@@ -244,55 +222,6 @@ function numberSourceLines(slice, firstLineNumber) {
|
|
|
244
222
|
}
|
|
245
223
|
return out.join('\n');
|
|
246
224
|
}
|
|
247
|
-
/**
|
|
248
|
-
* Mark a Claude session as having consulted MCP tools.
|
|
249
|
-
* This enables Grep/Glob/Bash commands that would otherwise be blocked.
|
|
250
|
-
*
|
|
251
|
-
* Why the explicit openSync + O_NOFOLLOW dance instead of plain writeFileSync:
|
|
252
|
-
* tmpdir() is world-writable on Linux (mode 1777), so on a shared multi-user
|
|
253
|
-
* machine any other local user can pre-create `codegraph-consulted-<hash>` as
|
|
254
|
-
* a symlink pointing at a file the victim owns. The old `writeFileSync` would
|
|
255
|
-
* happily follow that link and overwrite the target's contents with the ISO
|
|
256
|
-
* timestamp string (CWE-59). The session-id hash provides the predictability
|
|
257
|
-
* gate, but it's defense-in-depth: if a session id ever surfaces in logs,
|
|
258
|
-
* argv, or telemetry the attack becomes trivial, and the right fix is to not
|
|
259
|
-
* follow links from /tmp paths in the first place.
|
|
260
|
-
*/
|
|
261
|
-
function markSessionConsulted(sessionId) {
|
|
262
|
-
try {
|
|
263
|
-
const hash = (0, crypto_1.createHash)('md5').update(sessionId).digest('hex').slice(0, 16);
|
|
264
|
-
const markerPath = (0, path_1.join)((0, os_1.tmpdir)(), `codegraph-consulted-${hash}`);
|
|
265
|
-
// Refuse to follow a pre-planted symlink at the marker path (CWE-59).
|
|
266
|
-
// O_NOFOLLOW (below) is the atomic, TOCTOU-free guard on POSIX, but it is
|
|
267
|
-
// `undefined` on Windows (libuv ignores it), so the bitwise-OR silently
|
|
268
|
-
// drops it and openSync would follow the link. This lstat check closes that
|
|
269
|
-
// gap cross-platform; ENOENT (path is free) falls through to create it.
|
|
270
|
-
try {
|
|
271
|
-
if ((0, fs_1.lstatSync)(markerPath).isSymbolicLink())
|
|
272
|
-
return;
|
|
273
|
-
}
|
|
274
|
-
catch {
|
|
275
|
-
// No existing entry (or stat failed) — nothing to refuse; proceed.
|
|
276
|
-
}
|
|
277
|
-
// O_NOFOLLOW makes openSync throw ELOOP if markerPath is already a symlink.
|
|
278
|
-
// O_CREAT + O_TRUNC keep the original "create-or-overwrite" semantics, and
|
|
279
|
-
// mode 0o600 prevents readback by other local users (the marker payload is
|
|
280
|
-
// benign, but narrowing the exposure costs nothing).
|
|
281
|
-
const flags = fs_1.constants.O_WRONLY | fs_1.constants.O_CREAT | fs_1.constants.O_TRUNC | fs_1.constants.O_NOFOLLOW;
|
|
282
|
-
const fd = (0, fs_1.openSync)(markerPath, flags, 0o600);
|
|
283
|
-
try {
|
|
284
|
-
(0, fs_1.writeSync)(fd, new Date().toISOString());
|
|
285
|
-
}
|
|
286
|
-
finally {
|
|
287
|
-
(0, fs_1.closeSync)(fd);
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
catch {
|
|
291
|
-
// Silently fail - don't break MCP on marker write failure. ELOOP from a
|
|
292
|
-
// planted symlink lands here too, which is the intended behavior: refuse
|
|
293
|
-
// to write rather than overwrite an attacker-chosen target.
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
225
|
/**
|
|
297
226
|
* Per-file staleness banner emitted at the top of a tool response when the
|
|
298
227
|
* file watcher has pending events for files referenced by the response.
|
|
@@ -339,15 +268,16 @@ const projectPathProperty = {
|
|
|
339
268
|
/**
|
|
340
269
|
* All CodeGraph MCP tools
|
|
341
270
|
*
|
|
342
|
-
* Designed for minimal context usage - use
|
|
343
|
-
* and only use other tools for
|
|
271
|
+
* Designed for minimal context usage - use codegraph_explore as the primary tool
|
|
272
|
+
* (one call usually answers the whole question), and only use other tools for
|
|
273
|
+
* targeted follow-up queries.
|
|
344
274
|
*
|
|
345
275
|
* All tools support cross-project queries via the optional `projectPath` parameter.
|
|
346
276
|
*/
|
|
347
277
|
exports.tools = [
|
|
348
278
|
{
|
|
349
279
|
name: 'codegraph_search',
|
|
350
|
-
description: 'Quick symbol search by name. Returns locations only (no code). Use
|
|
280
|
+
description: 'Quick symbol search by name. Returns locations only (no code). Use codegraph_explore instead to get the actual source / understand an area in one call.',
|
|
351
281
|
inputSchema: {
|
|
352
282
|
type: 'object',
|
|
353
283
|
properties: {
|
|
@@ -370,34 +300,9 @@ exports.tools = [
|
|
|
370
300
|
required: ['query'],
|
|
371
301
|
},
|
|
372
302
|
},
|
|
373
|
-
{
|
|
374
|
-
name: 'codegraph_context',
|
|
375
|
-
description: 'PRIMARY TOOL — call FIRST for any "how does X work"/architecture/bug question. Returns entry points + related symbols + key code in one call; usually answers without further search/Read/Grep. Provides CODE context, not product requirements.',
|
|
376
|
-
inputSchema: {
|
|
377
|
-
type: 'object',
|
|
378
|
-
properties: {
|
|
379
|
-
task: {
|
|
380
|
-
type: 'string',
|
|
381
|
-
description: 'Description of the task, bug, or feature to build context for',
|
|
382
|
-
},
|
|
383
|
-
maxNodes: {
|
|
384
|
-
type: 'number',
|
|
385
|
-
description: 'Maximum symbols to include (default: 20)',
|
|
386
|
-
default: 20,
|
|
387
|
-
},
|
|
388
|
-
includeCode: {
|
|
389
|
-
type: 'boolean',
|
|
390
|
-
description: 'Include code snippets for key symbols (default: true)',
|
|
391
|
-
default: true,
|
|
392
|
-
},
|
|
393
|
-
projectPath: projectPathProperty,
|
|
394
|
-
},
|
|
395
|
-
required: ['task'],
|
|
396
|
-
},
|
|
397
|
-
},
|
|
398
303
|
{
|
|
399
304
|
name: 'codegraph_callers',
|
|
400
|
-
description: 'List functions that call <symbol>. For
|
|
305
|
+
description: 'List functions that call <symbol>. For the full flow, use codegraph_explore.',
|
|
401
306
|
inputSchema: {
|
|
402
307
|
type: 'object',
|
|
403
308
|
properties: {
|
|
@@ -417,7 +322,7 @@ exports.tools = [
|
|
|
417
322
|
},
|
|
418
323
|
{
|
|
419
324
|
name: 'codegraph_callees',
|
|
420
|
-
description: 'List functions that <symbol> calls. For
|
|
325
|
+
description: 'List functions that <symbol> calls. For the full flow, use codegraph_explore.',
|
|
421
326
|
inputSchema: {
|
|
422
327
|
type: 'object',
|
|
423
328
|
properties: {
|
|
@@ -457,7 +362,7 @@ exports.tools = [
|
|
|
457
362
|
},
|
|
458
363
|
{
|
|
459
364
|
name: 'codegraph_node',
|
|
460
|
-
description: '
|
|
365
|
+
description: 'SECONDARY (after codegraph_explore): get ONE symbol in full — its location, signature, callers/callees trail, and verbatim body (includeCode=true). When the name is AMBIGUOUS (an overloaded method, or the same method name on different types), it returns EVERY matching definition\'s full body in a single call — so you never need to Read a file to find the specific overload you want. For a heavily-overloaded name, pass `file` (and/or `line`) to pin the exact definition — e.g. the `file:line` a trail or another tool already showed you. Reach for this when explore trimmed a body you need. Use codegraph_explore for several related symbols or the full flow.',
|
|
461
366
|
inputSchema: {
|
|
462
367
|
type: 'object',
|
|
463
368
|
properties: {
|
|
@@ -470,6 +375,14 @@ exports.tools = [
|
|
|
470
375
|
description: 'Include full source code (default: false to minimize context)',
|
|
471
376
|
default: false,
|
|
472
377
|
},
|
|
378
|
+
file: {
|
|
379
|
+
type: 'string',
|
|
380
|
+
description: 'Optional: disambiguate an overloaded name to the definition in this file (path or basename, e.g. "harness.rs").',
|
|
381
|
+
},
|
|
382
|
+
line: {
|
|
383
|
+
type: 'number',
|
|
384
|
+
description: 'Optional: disambiguate to the definition at/around this line (use with the file:line a trail showed you).',
|
|
385
|
+
},
|
|
473
386
|
projectPath: projectPathProperty,
|
|
474
387
|
},
|
|
475
388
|
required: ['symbol'],
|
|
@@ -477,7 +390,7 @@ exports.tools = [
|
|
|
477
390
|
},
|
|
478
391
|
{
|
|
479
392
|
name: 'codegraph_explore',
|
|
480
|
-
description: '
|
|
393
|
+
description: 'PRIMARY TOOL — call FIRST for almost any question: how does X work, architecture, a bug, where/what is X, or surveying an area. Returns the verbatim source of the relevant symbols grouped by file in ONE capped call (Read-equivalent — do NOT re-open shown files). Query can be a natural-language question OR a bag of symbol/file names. Usually the ONLY call you need — answers without further search/node/Read/Grep.',
|
|
481
394
|
inputSchema: {
|
|
482
395
|
type: 'object',
|
|
483
396
|
properties: {
|
|
@@ -538,25 +451,6 @@ exports.tools = [
|
|
|
538
451
|
},
|
|
539
452
|
},
|
|
540
453
|
},
|
|
541
|
-
{
|
|
542
|
-
name: 'codegraph_trace',
|
|
543
|
-
description: 'Call path between two symbols — "how does <from> reach <to>?" Returns the chain with each hop\'s body inlined plus the destination\'s callees, in ONE call. Ideal for flow questions (update→render, request→handler, QuerySet→SQL). If no static path exists the chain broke at dynamic dispatch — the failure response inlines both endpoints + their TO-file siblings.',
|
|
544
|
-
inputSchema: {
|
|
545
|
-
type: 'object',
|
|
546
|
-
properties: {
|
|
547
|
-
from: {
|
|
548
|
-
type: 'string',
|
|
549
|
-
description: 'Symbol the flow starts at (e.g., "QuerySet", "handleRequest", "mutateElement")',
|
|
550
|
-
},
|
|
551
|
-
to: {
|
|
552
|
-
type: 'string',
|
|
553
|
-
description: 'Symbol the flow should reach (e.g., "execute_sql", "render", "setState")',
|
|
554
|
-
},
|
|
555
|
-
projectPath: projectPathProperty,
|
|
556
|
-
},
|
|
557
|
-
required: ['from', 'to'],
|
|
558
|
-
},
|
|
559
|
-
},
|
|
560
454
|
];
|
|
561
455
|
/**
|
|
562
456
|
* Allowlist-filtered tool definitions WITHOUT an engine — the static surface the
|
|
@@ -636,7 +530,7 @@ class ToolHandler {
|
|
|
636
530
|
* Unset/empty → every tool is exposed. Lets an operator (or an A/B harness)
|
|
637
531
|
* trim the tool surface without rebuilding the client config; the ablated
|
|
638
532
|
* tool is then truly absent from ListTools rather than merely denied on call.
|
|
639
|
-
* Matching is on the short form, so "
|
|
533
|
+
* Matching is on the short form, so "node" and "codegraph_node" both work.
|
|
640
534
|
*/
|
|
641
535
|
toolAllowlist() {
|
|
642
536
|
const raw = process.env.CODEGRAPH_MCP_TOOLS;
|
|
@@ -691,11 +585,9 @@ class ToolHandler {
|
|
|
691
585
|
// so it deserves the same gating.
|
|
692
586
|
const TINY_REPO_FILE_THRESHOLD = 500;
|
|
693
587
|
const TINY_REPO_CORE_TOOLS = new Set([
|
|
588
|
+
'codegraph_explore',
|
|
694
589
|
'codegraph_search',
|
|
695
|
-
'codegraph_context',
|
|
696
590
|
'codegraph_node',
|
|
697
|
-
'codegraph_explore',
|
|
698
|
-
'codegraph_trace',
|
|
699
591
|
]);
|
|
700
592
|
if (stats.fileCount < TINY_REPO_FILE_THRESHOLD) {
|
|
701
593
|
visible = visible.filter(t => TINY_REPO_CORE_TOOLS.has(t.name));
|
|
@@ -1004,9 +896,6 @@ class ToolHandler {
|
|
|
1004
896
|
case 'codegraph_search':
|
|
1005
897
|
result = await this.handleSearch(args);
|
|
1006
898
|
break;
|
|
1007
|
-
case 'codegraph_context':
|
|
1008
|
-
result = await this.handleContext(args);
|
|
1009
|
-
break;
|
|
1010
899
|
case 'codegraph_callers':
|
|
1011
900
|
result = await this.handleCallers(args);
|
|
1012
901
|
break;
|
|
@@ -1030,9 +919,6 @@ class ToolHandler {
|
|
|
1030
919
|
case 'codegraph_files':
|
|
1031
920
|
result = await this.handleFiles(args);
|
|
1032
921
|
break;
|
|
1033
|
-
case 'codegraph_trace':
|
|
1034
|
-
result = await this.handleTrace(args);
|
|
1035
|
-
break;
|
|
1036
922
|
default:
|
|
1037
923
|
return this.errorResult(`Unknown tool: ${toolName}`);
|
|
1038
924
|
}
|
|
@@ -1072,261 +958,6 @@ class ToolHandler {
|
|
|
1072
958
|
const formatted = this.formatSearchResults(ranked);
|
|
1073
959
|
return this.textResult(this.truncateOutput(formatted));
|
|
1074
960
|
}
|
|
1075
|
-
/**
|
|
1076
|
-
* Handle codegraph_context
|
|
1077
|
-
*/
|
|
1078
|
-
async handleContext(args) {
|
|
1079
|
-
const task = this.validateString(args.task, 'task');
|
|
1080
|
-
if (typeof task !== 'string')
|
|
1081
|
-
return task;
|
|
1082
|
-
// Mark session as consulted (enables Grep/Glob/Bash)
|
|
1083
|
-
const sessionId = process.env.CLAUDE_SESSION_ID;
|
|
1084
|
-
if (sessionId) {
|
|
1085
|
-
markSessionConsulted(sessionId);
|
|
1086
|
-
}
|
|
1087
|
-
const cg = this.getCodeGraph(args.projectPath);
|
|
1088
|
-
// On tiny repos (<150 files), trim maxNodes hard — the entire repo
|
|
1089
|
-
// is grep-able in a turn so a 20-node context is wasted budget.
|
|
1090
|
-
// 8 covers the typical 1-3 entry-point + their immediate neighbors
|
|
1091
|
-
// without dragging in the rest of the small codebase.
|
|
1092
|
-
let defaultMaxNodes = 20;
|
|
1093
|
-
let isTinyRepo = false;
|
|
1094
|
-
let isSmallRepo = false;
|
|
1095
|
-
try {
|
|
1096
|
-
const stats = cg.getStats();
|
|
1097
|
-
if (stats.fileCount < 150) {
|
|
1098
|
-
defaultMaxNodes = 8;
|
|
1099
|
-
isTinyRepo = true;
|
|
1100
|
-
}
|
|
1101
|
-
else if (stats.fileCount < 500) {
|
|
1102
|
-
isSmallRepo = true;
|
|
1103
|
-
}
|
|
1104
|
-
}
|
|
1105
|
-
catch {
|
|
1106
|
-
// stats failure — fall back to the standard default
|
|
1107
|
-
}
|
|
1108
|
-
const maxNodes = args.maxNodes || defaultMaxNodes;
|
|
1109
|
-
const includeCode = args.includeCode !== false;
|
|
1110
|
-
const context = await cg.buildContext(task, {
|
|
1111
|
-
maxNodes,
|
|
1112
|
-
includeCode,
|
|
1113
|
-
format: 'markdown',
|
|
1114
|
-
});
|
|
1115
|
-
// Detect if this looks like a feature request (vs bug fix or exploration)
|
|
1116
|
-
const isFeatureQuery = this.looksLikeFeatureRequest(task);
|
|
1117
|
-
const reminder = isFeatureQuery
|
|
1118
|
-
? '\n\n⚠️ **Ask user:** UX preferences, edge cases, acceptance criteria'
|
|
1119
|
-
: '';
|
|
1120
|
-
// Auto-trace for flow queries: when the task is asking "how does X
|
|
1121
|
-
// reach/flow/propagate from A to B", run the trace internally and
|
|
1122
|
-
// append its body to the context response. Saves the agent the
|
|
1123
|
-
// follow-up codegraph_trace call that was the #2 cost driver on
|
|
1124
|
-
// multi-module flow questions (Q3 / etcd Q2 in the audit).
|
|
1125
|
-
const flowTrace = await this.maybeInlineFlowTrace(task, cg);
|
|
1126
|
-
// Iter3 — sufficiency steering on small repos.
|
|
1127
|
-
//
|
|
1128
|
-
// Measured economics on tiny (<150) and small (<500) projects: every
|
|
1129
|
-
// additional MCP tool call costs ~$0.02-0.05 in cache-write tokens
|
|
1130
|
-
// (5K-15K per response at $3.75/1M). The agent reflexively follows
|
|
1131
|
-
// codegraph_context with explore/node even when the context response
|
|
1132
|
-
// is already sufficient — that pattern drove the cost gap that
|
|
1133
|
-
// smaller bodies (iter2) failed to close (smaller bodies just shifted
|
|
1134
|
-
// the agent to Read instead). Direct directive on small-repo
|
|
1135
|
-
// responses: tell the agent the context call IS the comprehensive
|
|
1136
|
-
// pass for a project of this size and that follow-ups should be
|
|
1137
|
-
// narrow (trace from→to, node single-symbol) — not another broad
|
|
1138
|
-
// explore that re-bundles the same content.
|
|
1139
|
-
// ITER4: unified strong directive for both tiny (<150) and small
|
|
1140
|
-
// (<500) tiers — measured iter3 result was that the soft <500
|
|
1141
|
-
// wording was IGNORED on sinatra (5 tool calls, +92% loss) while
|
|
1142
|
-
// the strong <150 wording was followed on cobra/slim (3 calls,
|
|
1143
|
-
// -21%/-22% wins). The single-file-framework problem (sinatra)
|
|
1144
|
-
// is structurally the same as cobra's; both deserve the same
|
|
1145
|
-
// sufficiency steering.
|
|
1146
|
-
let smallRepoTail = '';
|
|
1147
|
-
let smallRepoRouteInline = '';
|
|
1148
|
-
if (isTinyRepo || isSmallRepo) {
|
|
1149
|
-
// Iter12: backend-computed routing manifest for routing queries.
|
|
1150
|
-
// Builds a URL → handler map directly from the graph (each route
|
|
1151
|
-
// node has a `references` edge to its handler), then inlines the
|
|
1152
|
-
// top handler file's source. The agent gets the canonical
|
|
1153
|
-
// routing answer in one MCP call — no need to parse framework
|
|
1154
|
-
// DSL or grep for handlers.
|
|
1155
|
-
//
|
|
1156
|
-
// Replaces iter10's raw route-file inline. The manifest is more
|
|
1157
|
-
// information-dense (parsed URL→handler map vs raw config DSL)
|
|
1158
|
-
// and we still inline the top handler file's source so the agent
|
|
1159
|
-
// has the implementation bodies inline too.
|
|
1160
|
-
const isRouteQuery = /\b(route|routes|routing|request|handler|endpoint|api|controller|middleware|dispatch|invok)/i.test(task);
|
|
1161
|
-
if (isRouteQuery) {
|
|
1162
|
-
try {
|
|
1163
|
-
const manifest = cg.getRoutingManifest(40);
|
|
1164
|
-
if (manifest) {
|
|
1165
|
-
// 1) Compact URL→handler list (~30-60 lines, ~1-2KB).
|
|
1166
|
-
const lines = [
|
|
1167
|
-
`\n\n## Routing manifest (${manifest.totalRoutes} routes, top handler file holds ${manifest.topHandlerFileCount})`,
|
|
1168
|
-
'',
|
|
1169
|
-
'| URL | Handler | Location |',
|
|
1170
|
-
'|---|---|---|',
|
|
1171
|
-
];
|
|
1172
|
-
for (const e of manifest.entries) {
|
|
1173
|
-
lines.push(`| \`${e.url}\` | \`${e.handler}\` | ${e.handlerFile}:${e.handlerLine} |`);
|
|
1174
|
-
}
|
|
1175
|
-
// 2) Inline the top handler file's source.
|
|
1176
|
-
if (manifest.topHandlerFile && manifest.topHandlerFileCount >= 2) {
|
|
1177
|
-
try {
|
|
1178
|
-
const fullPath = pathModule.join(cg.getProjectRoot(), manifest.topHandlerFile);
|
|
1179
|
-
const stat = (0, fs_1.statSync)(fullPath);
|
|
1180
|
-
if (stat.size > 0 && stat.size <= 16000) {
|
|
1181
|
-
const source = (0, fs_1.readFileSync)(fullPath, 'utf-8');
|
|
1182
|
-
const capped = source.length > 7000 ? source.slice(0, 7000) + '\n... (truncated)' : source;
|
|
1183
|
-
const ext = (manifest.topHandlerFile.match(/\.([a-z]+)$/i)?.[1] || '').toLowerCase();
|
|
1184
|
-
const lang = ext === 'rb' ? 'ruby' : ext === 'py' ? 'python' :
|
|
1185
|
-
ext === 'go' ? 'go' : ext === 'rs' ? 'rust' :
|
|
1186
|
-
ext === 'js' || ext === 'jsx' ? 'javascript' :
|
|
1187
|
-
ext === 'ts' || ext === 'tsx' ? 'typescript' :
|
|
1188
|
-
ext === 'java' ? 'java' : ext === 'kt' ? 'kotlin' :
|
|
1189
|
-
ext === 'cs' ? 'csharp' : ext === 'php' ? 'php' :
|
|
1190
|
-
ext === 'swift' ? 'swift' : ext === 'yml' || ext === 'yaml' ? 'yaml' : '';
|
|
1191
|
-
lines.push('');
|
|
1192
|
-
lines.push(`### Top handler file (\`${manifest.topHandlerFile}\` — ${manifest.topHandlerFileCount}/${manifest.totalRoutes} routes, full source inlined — do NOT Read)`);
|
|
1193
|
-
lines.push('');
|
|
1194
|
-
lines.push('```' + lang);
|
|
1195
|
-
lines.push(capped);
|
|
1196
|
-
lines.push('```');
|
|
1197
|
-
}
|
|
1198
|
-
}
|
|
1199
|
-
catch { /* file read failed, skip the source inline */ }
|
|
1200
|
-
}
|
|
1201
|
-
smallRepoRouteInline = lines.join('\n');
|
|
1202
|
-
}
|
|
1203
|
-
}
|
|
1204
|
-
catch {
|
|
1205
|
-
// Manifest build failed — drop silently
|
|
1206
|
-
}
|
|
1207
|
-
}
|
|
1208
|
-
const sizeQualifier = isTinyRepo ? 'under 150' : 'under 500';
|
|
1209
|
-
const routingClause = smallRepoRouteInline
|
|
1210
|
-
? ' The URL→handler manifest and top handler file are also inlined above — answer routing questions from them.'
|
|
1211
|
-
: '';
|
|
1212
|
-
smallRepoTail = `\n\n---\n> **This project is small** (${sizeQualifier} indexed files). The entry points and code above cover the relevant surface — **do NOT call codegraph_explore as a follow-up; its content will largely duplicate this response**. If you need a specific flow, call \`codegraph_trace from→to\`. If you need one specific symbol's body, call \`codegraph_node <name>\`.${routingClause} Otherwise, answer from what is above.`;
|
|
1213
|
-
}
|
|
1214
|
-
// buildContext returns string when format is 'markdown'
|
|
1215
|
-
if (typeof context === 'string') {
|
|
1216
|
-
return this.textResult(this.truncateOutput(context + flowTrace + reminder + smallRepoRouteInline + smallRepoTail));
|
|
1217
|
-
}
|
|
1218
|
-
// If it returns TaskContext, format it
|
|
1219
|
-
return this.textResult(this.truncateOutput(this.formatTaskContext(context) + flowTrace + reminder + smallRepoRouteInline + smallRepoTail));
|
|
1220
|
-
}
|
|
1221
|
-
/**
|
|
1222
|
-
* Detect a flow-style task ("how does X reach Y", "trace the path from A to B")
|
|
1223
|
-
* and pre-run trace between the most likely endpoints, returning the trace
|
|
1224
|
-
* body to splice into the context response. Returns '' for non-flow queries
|
|
1225
|
-
* or when no plausible endpoint pair can be extracted.
|
|
1226
|
-
*
|
|
1227
|
-
* Conservative by design: only fires when the task has both a clear flow
|
|
1228
|
-
* keyword AND at least two distinct PascalCase / camelCase identifiers.
|
|
1229
|
-
* False positives waste a graph query; false negatives just fall back to
|
|
1230
|
-
* the agent calling trace itself (existing path-proximity wiring handles
|
|
1231
|
-
* disambiguation either way).
|
|
1232
|
-
*/
|
|
1233
|
-
async maybeInlineFlowTrace(task, cg) {
|
|
1234
|
-
const lower = task.toLowerCase();
|
|
1235
|
-
const FLOW_KEYWORDS = [
|
|
1236
|
-
'trace ',
|
|
1237
|
-
'from ',
|
|
1238
|
-
'reach ',
|
|
1239
|
-
'flow ',
|
|
1240
|
-
'propagat',
|
|
1241
|
-
'how does ',
|
|
1242
|
-
'how do ',
|
|
1243
|
-
];
|
|
1244
|
-
if (!FLOW_KEYWORDS.some((k) => lower.includes(k)))
|
|
1245
|
-
return '';
|
|
1246
|
-
// Extract candidate symbols — PascalCase or camelCase identifiers ≥3 chars.
|
|
1247
|
-
// Filter out common non-symbol words and the flow keywords themselves.
|
|
1248
|
-
const STOP_WORDS = new Set([
|
|
1249
|
-
'how', 'does', 'the', 'and', 'from', 'through', 'reach', 'reaches',
|
|
1250
|
-
'flow', 'path', 'trace', 'cross', 'module', 'modules', 'where',
|
|
1251
|
-
'update', 'updates', 'updated', 'when', 'what', 'this', 'that',
|
|
1252
|
-
]);
|
|
1253
|
-
const ids = [];
|
|
1254
|
-
const seen = new Set();
|
|
1255
|
-
const re = /\b([A-Z][a-z]+(?:[A-Z][a-z]*)+|[a-z]+[A-Z][a-z]*(?:[A-Z][a-z]*)*)\b/g;
|
|
1256
|
-
let m;
|
|
1257
|
-
while ((m = re.exec(task)) !== null) {
|
|
1258
|
-
const sym = m[1];
|
|
1259
|
-
if (sym.length < 3)
|
|
1260
|
-
continue;
|
|
1261
|
-
const key = sym.toLowerCase();
|
|
1262
|
-
if (STOP_WORDS.has(key) || seen.has(key))
|
|
1263
|
-
continue;
|
|
1264
|
-
seen.add(key);
|
|
1265
|
-
ids.push(sym);
|
|
1266
|
-
}
|
|
1267
|
-
if (ids.length < 2)
|
|
1268
|
-
return '';
|
|
1269
|
-
// The first two distinct symbols, in order of appearance, are the most
|
|
1270
|
-
// likely from/to endpoints — "from X ... through to Y" naturally places
|
|
1271
|
-
// them in that order in the prose. If the trace fails to connect, it
|
|
1272
|
-
// still returns the inlined endpoint bodies (the trace-failure rewrite).
|
|
1273
|
-
const fromSym = ids[0];
|
|
1274
|
-
const toSym = ids[1];
|
|
1275
|
-
let traceResult;
|
|
1276
|
-
try {
|
|
1277
|
-
traceResult = await this.handleTrace({
|
|
1278
|
-
from: fromSym,
|
|
1279
|
-
to: toSym,
|
|
1280
|
-
projectPath: cg.getProjectRoot(),
|
|
1281
|
-
});
|
|
1282
|
-
}
|
|
1283
|
-
catch {
|
|
1284
|
-
return '';
|
|
1285
|
-
}
|
|
1286
|
-
// Extract the textual body. Defensive: handleTrace's contract is the
|
|
1287
|
-
// standard tool-result shape used elsewhere in this file.
|
|
1288
|
-
const body = traceResult.content
|
|
1289
|
-
?.map((c) => (c.type === 'text' ? c.text : ''))
|
|
1290
|
-
.filter(Boolean)
|
|
1291
|
-
.join('\n')
|
|
1292
|
-
.trim();
|
|
1293
|
-
if (!body)
|
|
1294
|
-
return '';
|
|
1295
|
-
return [
|
|
1296
|
-
'',
|
|
1297
|
-
'## Inline flow trace',
|
|
1298
|
-
'',
|
|
1299
|
-
`Auto-traced \`${fromSym}\` → \`${toSym}\` because the query looks like a flow question. No follow-up codegraph_trace is needed for this pair.`,
|
|
1300
|
-
'',
|
|
1301
|
-
body,
|
|
1302
|
-
].join('\n');
|
|
1303
|
-
}
|
|
1304
|
-
/**
|
|
1305
|
-
* Heuristic to detect if a query looks like a feature request
|
|
1306
|
-
*/
|
|
1307
|
-
looksLikeFeatureRequest(task) {
|
|
1308
|
-
const featureKeywords = [
|
|
1309
|
-
'add', 'create', 'implement', 'build', 'enable', 'allow',
|
|
1310
|
-
'new feature', 'support for', 'ability to', 'want to',
|
|
1311
|
-
'should be able', 'need to add', 'swap', 'edit', 'modify'
|
|
1312
|
-
];
|
|
1313
|
-
const bugKeywords = [
|
|
1314
|
-
'fix', 'bug', 'error', 'broken', 'crash', 'issue', 'problem',
|
|
1315
|
-
'not working', 'fails', 'undefined', 'null'
|
|
1316
|
-
];
|
|
1317
|
-
const explorationKeywords = [
|
|
1318
|
-
'how does', 'where is', 'what is', 'find', 'show me',
|
|
1319
|
-
'explain', 'understand', 'explore'
|
|
1320
|
-
];
|
|
1321
|
-
const lowerTask = task.toLowerCase();
|
|
1322
|
-
// If it's clearly a bug or exploration, not a feature
|
|
1323
|
-
if (bugKeywords.some(k => lowerTask.includes(k)))
|
|
1324
|
-
return false;
|
|
1325
|
-
if (explorationKeywords.some(k => lowerTask.includes(k)))
|
|
1326
|
-
return false;
|
|
1327
|
-
// If it matches feature keywords, it's likely a feature request
|
|
1328
|
-
return featureKeywords.some(k => lowerTask.includes(k));
|
|
1329
|
-
}
|
|
1330
961
|
/**
|
|
1331
962
|
* Handle codegraph_callers
|
|
1332
963
|
*/
|
|
@@ -1425,295 +1056,6 @@ class ToolHandler {
|
|
|
1425
1056
|
const formatted = this.formatImpact(symbol, mergedImpact) + allMatches.note;
|
|
1426
1057
|
return this.textResult(this.truncateOutput(formatted));
|
|
1427
1058
|
}
|
|
1428
|
-
/**
|
|
1429
|
-
* Handle codegraph_trace — shortest CALL PATH between two symbols.
|
|
1430
|
-
*
|
|
1431
|
-
* Exposes GraphTraverser.findPath: the chain of functions from `from` to `to`,
|
|
1432
|
-
* each hop annotated with file:line and the call-site line. This is the
|
|
1433
|
-
* capability grep/Read structurally cannot provide. When no static path
|
|
1434
|
-
* exists, the chain has almost certainly broken at dynamic dispatch
|
|
1435
|
-
* (callbacks, descriptors, metaclasses) — we say so and surface the start
|
|
1436
|
-
* symbol's outgoing calls so the agent bridges the one missing hop with
|
|
1437
|
-
* codegraph_node rather than blindly reading.
|
|
1438
|
-
*/
|
|
1439
|
-
async handleTrace(args) {
|
|
1440
|
-
const from = this.validateString(args.from, 'from');
|
|
1441
|
-
if (typeof from !== 'string')
|
|
1442
|
-
return from;
|
|
1443
|
-
const to = this.validateString(args.to, 'to');
|
|
1444
|
-
if (typeof to !== 'string')
|
|
1445
|
-
return to;
|
|
1446
|
-
const cg = this.getCodeGraph(args.projectPath);
|
|
1447
|
-
const fromMatches = this.findAllSymbols(cg, from);
|
|
1448
|
-
if (fromMatches.nodes.length === 0)
|
|
1449
|
-
return this.textResult(`Symbol "${from}" not found in the codebase`);
|
|
1450
|
-
const toMatches = this.findAllSymbols(cg, to);
|
|
1451
|
-
if (toMatches.nodes.length === 0)
|
|
1452
|
-
return this.textResult(`Symbol "${to}" not found in the codebase`);
|
|
1453
|
-
// Trace along call edges only — a true call path. Names can map to several
|
|
1454
|
-
// nodes, so try a few from×to candidate pairs until a usable path turns up.
|
|
1455
|
-
//
|
|
1456
|
-
// MAX_HOPS guard: a BFS shortest path longer than this on a dense call graph
|
|
1457
|
-
// is almost always a spurious wander through unrelated code (django's
|
|
1458
|
-
// `_fetch_all → … → execute_sql` BFS detours through prefetch/filter), not
|
|
1459
|
-
// the real execution flow — and a confident-but-wrong 15-hop trace is worse
|
|
1460
|
-
// than none. Over-cap paths are rejected and reported as "no direct path"
|
|
1461
|
-
// (which, on real code, means the flow breaks at dynamic dispatch).
|
|
1462
|
-
const edgeKinds = ['calls'];
|
|
1463
|
-
const MAX_HOPS = 7;
|
|
1464
|
-
// Path-proximity pairing: in a multi-module repo a symbol name like
|
|
1465
|
-
// `EndBlocker` exists in 20+ modules. FTS picks one almost arbitrarily;
|
|
1466
|
-
// the WRONG pair (e.g. simapp's wrapper EndBlocker paired with gov's Tally)
|
|
1467
|
-
// has no static path, falls through to the dynamic-dispatch failure branch,
|
|
1468
|
-
// and surfaces unrelated bodies — exactly the cosmos-Q3 trace failure mode.
|
|
1469
|
-
// Score every from×to combo by shared file-path prefix length; try the
|
|
1470
|
-
// most-co-located pair first (e.g. `x/gov/abci.go::EndBlocker` ×
|
|
1471
|
-
// `x/gov/keeper/tally.go::Tally` share `x/gov/`).
|
|
1472
|
-
//
|
|
1473
|
-
// Consider the FULL candidate set, not just the FTS top-5: the right
|
|
1474
|
-
// EndBlocker for a gov-module flow may rank 8th in FTS but share the
|
|
1475
|
-
// entire `x/gov/` prefix with the destination. Path-proximity supersedes
|
|
1476
|
-
// FTS for this disambiguation. Findpath trials are still capped by
|
|
1477
|
-
// FINDPATH_PAIR_BUDGET below to bound graph traversal cost.
|
|
1478
|
-
const sharedDirPrefixLen = (a, b) => {
|
|
1479
|
-
const aDir = a.replace(/[^/]+$/, '');
|
|
1480
|
-
const bDir = b.replace(/[^/]+$/, '');
|
|
1481
|
-
let i = 0;
|
|
1482
|
-
while (i < aDir.length && i < bDir.length && aDir[i] === bDir[i])
|
|
1483
|
-
i++;
|
|
1484
|
-
return i;
|
|
1485
|
-
};
|
|
1486
|
-
// Cosmos-Q3 surfaced a second-order failure: `enterprise/group/x/group/`
|
|
1487
|
-
// SHARES MORE of its path with `enterprise/group/x/group/keeper/tally.go`
|
|
1488
|
-
// (24 chars) than `x/gov/abci.go` shares with `x/gov/keeper/tally.go`
|
|
1489
|
-
// (6 chars), so pure shared-prefix prefers the side-experiment module
|
|
1490
|
-
// over the canonical one — even though the user's question is clearly
|
|
1491
|
-
// about the main gov module. Penalize candidates living under prefixes
|
|
1492
|
-
// that conventionally hold extensions / experiments / vendored code, so
|
|
1493
|
-
// the canonical-path pair wins even when its shared prefix is short.
|
|
1494
|
-
const isLessCanonicalPath = (p) => /^(enterprise|contrib|examples?|sample|playground|vendor|third[_-]?party|deprecated|legacy)\//i.test(p);
|
|
1495
|
-
const LESS_CANONICAL_PENALTY = 100; // any canonical candidate beats any less-canonical one
|
|
1496
|
-
const scorePair = (a, b) => sharedDirPrefixLen(a, b)
|
|
1497
|
-
- (isLessCanonicalPath(a) ? LESS_CANONICAL_PENALTY : 0)
|
|
1498
|
-
- (isLessCanonicalPath(b) ? LESS_CANONICAL_PENALTY : 0);
|
|
1499
|
-
const fromCands = fromMatches.nodes;
|
|
1500
|
-
const toCands = toMatches.nodes;
|
|
1501
|
-
// Candidate relevance: an overloaded name (Alamofire has 44 `request`s, most
|
|
1502
|
-
// of them EMPTY EventMonitor protocol-conformance stubs `func request(…){}`)
|
|
1503
|
-
// floods the pool with no-op decls. Shared-dir-prefix alone then MISLEADS —
|
|
1504
|
-
// two unrelated `Source/Features/` delegate stubs outscore the real
|
|
1505
|
-
// `Source/Core/Session.request` × `Source/Core/…task` pair the agent meant,
|
|
1506
|
-
// so trace resolves to stubs, finds no path, and the agent reads by line.
|
|
1507
|
-
// Penalize empty stubs and test-file symbols so a substantive entry point
|
|
1508
|
-
// wins; among real methods this is ~flat, so path-proximity still decides
|
|
1509
|
-
// (cosmos EndBlocker disambiguation is unaffected — none of its candidates
|
|
1510
|
-
// are stubs/tests).
|
|
1511
|
-
const isTestPath = (p) => /(^|\/)(tests?|specs?|__tests__|testdata|mocks?|fixtures?)\//i.test(p) || /\.(test|spec)\.[a-z]+$/i.test(p);
|
|
1512
|
-
const nodeRelevance = (n) => {
|
|
1513
|
-
const bodyLines = Math.max(0, (n.endLine ?? n.startLine) - n.startLine);
|
|
1514
|
-
let s = Math.min(bodyLines, 20); // a substantive body is more likely the meant symbol
|
|
1515
|
-
if (bodyLines <= 1)
|
|
1516
|
-
s -= 40; // empty/one-line stub (protocol no-op, decl-only) — almost never the trace endpoint
|
|
1517
|
-
if (isTestPath(n.filePath))
|
|
1518
|
-
s -= 150; // a Source/ symbol is meant over a Tests/ same-named one
|
|
1519
|
-
return s;
|
|
1520
|
-
};
|
|
1521
|
-
const pairs = [];
|
|
1522
|
-
for (const f of fromCands) {
|
|
1523
|
-
for (const t of toCands) {
|
|
1524
|
-
pairs.push({ f, t, score: scorePair(f.filePath, t.filePath) + nodeRelevance(f) + nodeRelevance(t) });
|
|
1525
|
-
}
|
|
1526
|
-
}
|
|
1527
|
-
// Sort by shared prefix desc, then by FTS order (already encoded in the
|
|
1528
|
-
// pairs' insertion order — both for f and t). The tiebreaker preserves
|
|
1529
|
-
// findAllSymbols' generated-file-last ranking.
|
|
1530
|
-
pairs.sort((a, b) => b.score - a.score);
|
|
1531
|
-
// Cap how many graph-path probes we attempt so a 50×50 cross-product
|
|
1532
|
-
// doesn't blow up on a god-named symbol like `Get` (well-named flows have
|
|
1533
|
-
// their good pair near the top of the sort anyway).
|
|
1534
|
-
const FINDPATH_PAIR_BUDGET = 20;
|
|
1535
|
-
const fromTry = fromCands;
|
|
1536
|
-
const toTry = toCands;
|
|
1537
|
-
let path = null;
|
|
1538
|
-
let overCap = null;
|
|
1539
|
-
let bestPair = null;
|
|
1540
|
-
let triedPairs = 0;
|
|
1541
|
-
for (const { f, t } of pairs) {
|
|
1542
|
-
if (path)
|
|
1543
|
-
break;
|
|
1544
|
-
if (triedPairs >= FINDPATH_PAIR_BUDGET)
|
|
1545
|
-
break;
|
|
1546
|
-
triedPairs++;
|
|
1547
|
-
const p = cg.findPath(f.id, t.id, edgeKinds);
|
|
1548
|
-
if (p && p.length > 1) {
|
|
1549
|
-
if (p.length <= MAX_HOPS) {
|
|
1550
|
-
path = p;
|
|
1551
|
-
bestPair = { f, t };
|
|
1552
|
-
break;
|
|
1553
|
-
}
|
|
1554
|
-
if (!overCap || p.length < overCap.length) {
|
|
1555
|
-
overCap = p;
|
|
1556
|
-
bestPair = { f, t };
|
|
1557
|
-
}
|
|
1558
|
-
}
|
|
1559
|
-
else if (!bestPair) {
|
|
1560
|
-
// No path yet — remember the top-scored pair so the failure branch
|
|
1561
|
-
// surfaces the most-co-located candidates' bodies, not whatever FTS
|
|
1562
|
-
// happened to put first.
|
|
1563
|
-
bestPair = { f, t };
|
|
1564
|
-
}
|
|
1565
|
-
}
|
|
1566
|
-
if (!path) {
|
|
1567
|
-
// No static path — almost always a dynamic-dispatch break. INSTEAD of
|
|
1568
|
-
// telling the agent to chase the gap with codegraph_node/callers/callees
|
|
1569
|
-
// (which fans out into 3-4 follow-up tool calls + a Read), inline the
|
|
1570
|
-
// material those would have returned right here. Measured on cosmos-Q3:
|
|
1571
|
-
// the failed-trace + subsequent fan-out used to cost ~2× a single
|
|
1572
|
-
// sufficient trace call; this branch closes that gap.
|
|
1573
|
-
// Prefer the path-proximity-best pair we identified above (e.g. gov's
|
|
1574
|
-
// EndBlocker × gov's Tally) over the FTS top-pick (simapp's wrapper).
|
|
1575
|
-
const start = bestPair?.f ?? fromTry[0];
|
|
1576
|
-
const end = bestPair?.t ?? toTry[0];
|
|
1577
|
-
const fileCache = new Map();
|
|
1578
|
-
const lines = [
|
|
1579
|
-
`No direct static call path from "${from}" to "${to}" — the chain almost certainly breaks at dynamic dispatch (a callback / interface dispatch / framework hook / metaclass). Both endpoint bodies + their immediate neighbors are inlined below; answer from them — a follow-up codegraph_node/callers/callees on these would just return what is already here.`,
|
|
1580
|
-
'',
|
|
1581
|
-
];
|
|
1582
|
-
if (overCap) {
|
|
1583
|
-
lines.push(`> Indirect chain of ${overCap.length} hops exists but is over the ${MAX_HOPS}-hop cap (usually a BFS wander through unrelated code, not the real execution flow).`, '');
|
|
1584
|
-
}
|
|
1585
|
-
// Track which node IDs we've already inlined a body for so we don't
|
|
1586
|
-
// double-emit when a callee of FROM is also surfaced separately.
|
|
1587
|
-
const inlinedBodies = new Set();
|
|
1588
|
-
const inlineBody = (n, lineCap, charCap) => {
|
|
1589
|
-
if (inlinedBodies.has(n.id))
|
|
1590
|
-
return false;
|
|
1591
|
-
inlinedBodies.add(n.id);
|
|
1592
|
-
const body = this.sourceRangeAt(cg, n.filePath, n.startLine, n.endLine, fileCache, lineCap, charCap);
|
|
1593
|
-
if (body) {
|
|
1594
|
-
lines.push(body);
|
|
1595
|
-
return true;
|
|
1596
|
-
}
|
|
1597
|
-
return false;
|
|
1598
|
-
};
|
|
1599
|
-
const inlineEndpoint = (label, node) => {
|
|
1600
|
-
lines.push(`### ${label}: \`${node.name}\` (${node.filePath}:${node.startLine}-${node.endLine})`);
|
|
1601
|
-
inlineBody(node, 120, 3600);
|
|
1602
|
-
const callers = cg.getCallers(node.id).slice(0, 6);
|
|
1603
|
-
if (callers.length > 0) {
|
|
1604
|
-
lines.push(`**Callers of \`${node.name}\`:** ` +
|
|
1605
|
-
callers.map(c => `${c.node.name} (${c.node.filePath}:${c.node.startLine})`).join(', '));
|
|
1606
|
-
}
|
|
1607
|
-
const callees = cg.getCallees(node.id).slice(0, 8);
|
|
1608
|
-
if (callees.length > 0) {
|
|
1609
|
-
lines.push(`**\`${node.name}\` calls:** ` +
|
|
1610
|
-
callees.map(c => `${c.node.name} (${c.node.filePath}:${c.node.startLine})`).join(', '));
|
|
1611
|
-
}
|
|
1612
|
-
lines.push('');
|
|
1613
|
-
};
|
|
1614
|
-
inlineEndpoint('FROM', start);
|
|
1615
|
-
if (end.id !== start.id)
|
|
1616
|
-
inlineEndpoint('TO', end);
|
|
1617
|
-
// Inline the OTHER top-level functions/methods in TO's file — that's
|
|
1618
|
-
// where the missing dynamic-dispatch flow usually lives. Concrete
|
|
1619
|
-
// measurement from cosmos-Q1: `msgServer.Send` statically calls only
|
|
1620
|
-
// utility functions (`StringToBytes`, `Wrapf`); its real next-hop
|
|
1621
|
-
// `SendCoins` is invoked via an embedded-interface call (`k.Keeper.SendCoins`)
|
|
1622
|
-
// that static parsing CAN'T see. The flow IS in the same file as the
|
|
1623
|
-
// destination (`x/bank/keeper/send.go`: SendCoins → subUnlockedCoins →
|
|
1624
|
-
// addCoins → setBalance). Pre-inlining those file-mates is what
|
|
1625
|
-
// replaces the agent's "trace fail → search SendCoins → node SendCoins
|
|
1626
|
-
// → trace again" fan-out.
|
|
1627
|
-
const NEIGHBOR_LINES = 40;
|
|
1628
|
-
const NEIGHBOR_CHARS = 1200;
|
|
1629
|
-
const NEIGHBOR_K = 5;
|
|
1630
|
-
const fileSiblings = (anchor) => {
|
|
1631
|
-
// Functions and methods in the same file as the anchor, excluding
|
|
1632
|
-
// the anchor itself and anything we've already inlined. Sort by
|
|
1633
|
-
// distance from the anchor's startLine so the closest symbols come
|
|
1634
|
-
// first (the flow is usually adjacent in the file).
|
|
1635
|
-
const sameFile = cg
|
|
1636
|
-
.getNodesByKind('function')
|
|
1637
|
-
.filter((n) => n.filePath === anchor.filePath)
|
|
1638
|
-
.concat(cg.getNodesByKind('method').filter((n) => n.filePath === anchor.filePath));
|
|
1639
|
-
return sameFile
|
|
1640
|
-
.filter((n) => n.id !== anchor.id && !inlinedBodies.has(n.id))
|
|
1641
|
-
.sort((a, b) => Math.abs(a.startLine - anchor.startLine) - Math.abs(b.startLine - anchor.startLine))
|
|
1642
|
-
.slice(0, NEIGHBOR_K);
|
|
1643
|
-
};
|
|
1644
|
-
const renderSiblings = (label, siblings) => {
|
|
1645
|
-
if (siblings.length === 0)
|
|
1646
|
-
return;
|
|
1647
|
-
lines.push(`### ${label}`);
|
|
1648
|
-
for (const sib of siblings) {
|
|
1649
|
-
lines.push('');
|
|
1650
|
-
lines.push(`- \`${sib.name}\` (${sib.filePath}:${sib.startLine}-${sib.endLine})`);
|
|
1651
|
-
inlineBody(sib, NEIGHBOR_LINES, NEIGHBOR_CHARS);
|
|
1652
|
-
}
|
|
1653
|
-
lines.push('');
|
|
1654
|
-
};
|
|
1655
|
-
renderSiblings(`Other functions in \`${end.filePath}\` (the flow that the dynamic-dispatch hop reaches — bodies inlined)`, fileSiblings(end));
|
|
1656
|
-
lines.push('> Endpoint bodies + the other functions in the destination\'s file are inlined above. Together they typically cover the missing dynamic-dispatch boundary (interface-method calls like `k.Keeper.SendCoins` that static parsing can\'t follow). **No further codegraph_node / codegraph_callers / codegraph_callees / Read / Grep is needed for any symbol already shown here** — call them again only if you need to walk DEEPER than what is inlined.');
|
|
1657
|
-
return this.textResult(this.truncateOutput(lines.join('\n') + fromMatches.note + toMatches.note));
|
|
1658
|
-
}
|
|
1659
|
-
const lines = [
|
|
1660
|
-
`## Trace: ${from} → ${to}`,
|
|
1661
|
-
'',
|
|
1662
|
-
`Full execution path below — ${path.length} hops, each with its body, plus what the destination calls. This is the complete flow; answer from it.`,
|
|
1663
|
-
'',
|
|
1664
|
-
`${path.length} hops:`,
|
|
1665
|
-
'',
|
|
1666
|
-
];
|
|
1667
|
-
// Inline what each hop needs so the agent doesn't Read/Grep to get it: the
|
|
1668
|
-
// call-site source line, the registration site for dynamic-dispatch hops, AND
|
|
1669
|
-
// the hop's own body (capped per hop so the trace stays path-scoped). Earlier
|
|
1670
|
-
// versions inlined only the call-site line, which left agents calling explore
|
|
1671
|
-
// or Read for the bodies — the exact follow-up the ablation experiment measured.
|
|
1672
|
-
const fileCache = new Map();
|
|
1673
|
-
for (let i = 0; i < path.length; i++) {
|
|
1674
|
-
const step = path[i];
|
|
1675
|
-
if (step.edge) {
|
|
1676
|
-
const synth = this.synthEdgeNote(step.edge);
|
|
1677
|
-
if (synth) {
|
|
1678
|
-
lines.push(` ↓ ${synth.label}`);
|
|
1679
|
-
if (synth.registeredAt) {
|
|
1680
|
-
const regSrc = this.sourceLineAt(cg, synth.registeredAt, fileCache);
|
|
1681
|
-
lines.push(` ↳ registered at ${synth.registeredAt}${regSrc ? ` ${regSrc}` : ''}`);
|
|
1682
|
-
}
|
|
1683
|
-
}
|
|
1684
|
-
else {
|
|
1685
|
-
// The call happens in the PREVIOUS hop's file at edge.line.
|
|
1686
|
-
const prev = path[i - 1];
|
|
1687
|
-
const ref = prev && step.edge.line ? `${prev.node.filePath}:${step.edge.line}` : undefined;
|
|
1688
|
-
const callSrc = this.sourceLineAt(cg, ref, fileCache);
|
|
1689
|
-
lines.push(` ↓ ${step.edge.kind}${step.edge.line ? `@${step.edge.line}` : ''}${callSrc ? ` ${callSrc}` : ''}`);
|
|
1690
|
-
}
|
|
1691
|
-
}
|
|
1692
|
-
lines.push(`${i + 1}. ${step.node.name} (${step.node.filePath}:${step.node.startLine}-${step.node.endLine})`);
|
|
1693
|
-
const body = this.sourceRangeAt(cg, step.node.filePath, step.node.startLine, step.node.endLine, fileCache, 60, 1800);
|
|
1694
|
-
if (body)
|
|
1695
|
-
lines.push(body);
|
|
1696
|
-
}
|
|
1697
|
-
// The "last mile": what the destination does next. Agents otherwise explore/Read
|
|
1698
|
-
// for exactly this (e.g. renderStaticScene → _renderStaticScene → the canvas draw),
|
|
1699
|
-
// so inlining the destination's callees is what actually stops the investigation —
|
|
1700
|
-
// sufficiency, not a "don't explore" instruction.
|
|
1701
|
-
const dest = path[path.length - 1].node;
|
|
1702
|
-
const destCallees = cg.getCallees(dest.id)
|
|
1703
|
-
.filter(c => !path.some(p => p.node.id === c.node.id))
|
|
1704
|
-
.slice(0, 6);
|
|
1705
|
-
if (destCallees.length > 0) {
|
|
1706
|
-
lines.push('', `### \`${dest.name}\` then calls (the destination's immediate work):`);
|
|
1707
|
-
for (const c of destCallees) {
|
|
1708
|
-
lines.push('', `- ${c.node.name} (${c.node.filePath}:${c.node.startLine}-${c.node.endLine})`);
|
|
1709
|
-
const body = this.sourceRangeAt(cg, c.node.filePath, c.node.startLine, c.node.endLine, fileCache, 16, 600);
|
|
1710
|
-
if (body)
|
|
1711
|
-
lines.push(body);
|
|
1712
|
-
}
|
|
1713
|
-
}
|
|
1714
|
-
lines.push('', '> Full path + every hop body + the destination\'s calls are inlined above — the complete flow. Answer from it; a Read is only needed to chase a specific local variable\'s data-flow.');
|
|
1715
|
-
return this.textResult(this.truncateOutput(lines.join('\n')));
|
|
1716
|
-
}
|
|
1717
1059
|
/**
|
|
1718
1060
|
* Describe a synthesized (dynamic-dispatch) edge for human output: how the
|
|
1719
1061
|
* callback was wired up — the bridge static parsing can't see. Returns null
|
|
@@ -1783,82 +1125,6 @@ class ToolHandler {
|
|
|
1783
1125
|
}
|
|
1784
1126
|
return null;
|
|
1785
1127
|
}
|
|
1786
|
-
/**
|
|
1787
|
-
* Read one trimmed source line at "relpath:line" (relative to the project
|
|
1788
|
-
* root). `cache` holds split file contents so a multi-hop trace reads each
|
|
1789
|
-
* file at most once. Returns null if the file/line can't be resolved.
|
|
1790
|
-
*/
|
|
1791
|
-
sourceLineAt(cg, ref, cache) {
|
|
1792
|
-
if (!ref)
|
|
1793
|
-
return null;
|
|
1794
|
-
const i = ref.lastIndexOf(':');
|
|
1795
|
-
if (i < 0)
|
|
1796
|
-
return null;
|
|
1797
|
-
const filePath = ref.slice(0, i);
|
|
1798
|
-
const line = parseInt(ref.slice(i + 1), 10);
|
|
1799
|
-
if (!Number.isFinite(line) || line < 1)
|
|
1800
|
-
return null;
|
|
1801
|
-
let fileLines = cache.get(filePath);
|
|
1802
|
-
if (!fileLines) {
|
|
1803
|
-
const abs = (0, utils_1.validatePathWithinRoot)(cg.getProjectRoot(), filePath);
|
|
1804
|
-
if (!abs || !(0, fs_1.existsSync)(abs))
|
|
1805
|
-
return null;
|
|
1806
|
-
try {
|
|
1807
|
-
fileLines = (0, fs_1.readFileSync)(abs, 'utf-8').split('\n');
|
|
1808
|
-
}
|
|
1809
|
-
catch {
|
|
1810
|
-
return null;
|
|
1811
|
-
}
|
|
1812
|
-
cache.set(filePath, fileLines);
|
|
1813
|
-
}
|
|
1814
|
-
const raw = fileLines[line - 1];
|
|
1815
|
-
if (raw == null)
|
|
1816
|
-
return null;
|
|
1817
|
-
const t = raw.trim();
|
|
1818
|
-
return t.length > 160 ? t.slice(0, 157) + '…' : t;
|
|
1819
|
-
}
|
|
1820
|
-
/**
|
|
1821
|
-
* Read a hop's body — filePath lines [startLine..endLine] — for inlining into
|
|
1822
|
-
* a trace, capped (lines + chars) so the whole path stays path-scoped even on
|
|
1823
|
-
* a 7-hop chain. Dedents to the body's own indentation and marks truncation.
|
|
1824
|
-
* Shares `cache` with sourceLineAt so each file is read at most once per trace.
|
|
1825
|
-
*/
|
|
1826
|
-
sourceRangeAt(cg, filePath, startLine, endLine, cache, maxLines = 28, maxChars = 1200) {
|
|
1827
|
-
if (!Number.isFinite(startLine) || startLine < 1)
|
|
1828
|
-
return null;
|
|
1829
|
-
let fileLines = cache.get(filePath);
|
|
1830
|
-
if (!fileLines) {
|
|
1831
|
-
const abs = (0, utils_1.validatePathWithinRoot)(cg.getProjectRoot(), filePath);
|
|
1832
|
-
if (!abs || !(0, fs_1.existsSync)(abs))
|
|
1833
|
-
return null;
|
|
1834
|
-
try {
|
|
1835
|
-
fileLines = (0, fs_1.readFileSync)(abs, 'utf-8').split('\n');
|
|
1836
|
-
}
|
|
1837
|
-
catch {
|
|
1838
|
-
return null;
|
|
1839
|
-
}
|
|
1840
|
-
cache.set(filePath, fileLines);
|
|
1841
|
-
}
|
|
1842
|
-
const end = Number.isFinite(endLine) && endLine >= startLine ? endLine : startLine;
|
|
1843
|
-
let slice = fileLines.slice(startLine - 1, end);
|
|
1844
|
-
if (slice.length === 0)
|
|
1845
|
-
return null;
|
|
1846
|
-
let omitted = 0;
|
|
1847
|
-
if (slice.length > maxLines) {
|
|
1848
|
-
omitted = slice.length - maxLines;
|
|
1849
|
-
slice = slice.slice(0, maxLines);
|
|
1850
|
-
}
|
|
1851
|
-
const nonBlank = slice.filter(l => l.trim().length > 0);
|
|
1852
|
-
const dedent = nonBlank.length ? Math.min(...nonBlank.map(l => l.length - l.trimStart().length)) : 0;
|
|
1853
|
-
let text = slice.map((l, i) => ` ${startLine + i}\t${l.slice(dedent)}`).join('\n');
|
|
1854
|
-
if (text.length > maxChars) {
|
|
1855
|
-
text = text.slice(0, maxChars).replace(/\n[^\n]*$/, '');
|
|
1856
|
-
omitted = Math.max(omitted, 1);
|
|
1857
|
-
}
|
|
1858
|
-
if (omitted > 0)
|
|
1859
|
-
text += `\n … (+${omitted} more line${omitted === 1 ? '' : 's'})`;
|
|
1860
|
-
return text;
|
|
1861
|
-
}
|
|
1862
1128
|
/**
|
|
1863
1129
|
* Flow-from-named-symbols: an agent's codegraph_explore query is a bag of
|
|
1864
1130
|
* symbol names that usually spans the flow it's investigating (e.g.
|
|
@@ -2019,7 +1285,7 @@ class ToolHandler {
|
|
|
2019
1285
|
if (synthLines.length) {
|
|
2020
1286
|
out.push('## Dynamic-dispatch links among your symbols', '(synthesized — the indirect hops grep/Read would reconstruct; the `@file:line` is the wiring site)', '', ...synthLines, '');
|
|
2021
1287
|
}
|
|
2022
|
-
out.push('> Full source for these symbols is below
|
|
1288
|
+
out.push('> Full source for these symbols is below — the call flow among them, followed by their bodies.', '');
|
|
2023
1289
|
// namedNodeIds = every callable the agent explicitly named (a superset of
|
|
2024
1290
|
// the spine). A file holding one is something the agent asked to SEE, so it
|
|
2025
1291
|
// must keep full source even if it's an off-spine polymorphic sibling — the
|
|
@@ -2031,6 +1297,147 @@ class ToolHandler {
|
|
|
2031
1297
|
return EMPTY;
|
|
2032
1298
|
}
|
|
2033
1299
|
}
|
|
1300
|
+
/**
|
|
1301
|
+
* Compact "blast radius" for the entry symbols of an explore result: who
|
|
1302
|
+
* depends on each (callers) and which test files cover it — LOCATIONS ONLY,
|
|
1303
|
+
* no source, so the agent knows what to update / re-verify before editing
|
|
1304
|
+
* without reaching for a separate impact call. Always-on, but skips symbols
|
|
1305
|
+
* that have no dependents (nothing to warn about), and returns '' when none
|
|
1306
|
+
* qualify so a leaf-only exploration stays clean.
|
|
1307
|
+
*/
|
|
1308
|
+
buildBlastRadiusSection(cg, subgraph) {
|
|
1309
|
+
const ROOT_CAP = 5; // only the symbols the query actually targeted
|
|
1310
|
+
const FILE_CAP = 4; // caller files listed per symbol before "+N more"
|
|
1311
|
+
const MEANINGFUL = new Set([
|
|
1312
|
+
'function', 'method', 'class', 'interface', 'struct', 'trait', 'protocol',
|
|
1313
|
+
'enum', 'type_alias', 'component', 'constant', 'variable', 'property', 'field',
|
|
1314
|
+
]);
|
|
1315
|
+
const rel = (p) => p.replace(/\\/g, '/');
|
|
1316
|
+
const roots = subgraph.roots
|
|
1317
|
+
.map((id) => subgraph.nodes.get(id))
|
|
1318
|
+
.filter((n) => !!n && MEANINGFUL.has(n.kind))
|
|
1319
|
+
.slice(0, ROOT_CAP);
|
|
1320
|
+
if (roots.length === 0)
|
|
1321
|
+
return '';
|
|
1322
|
+
const entries = [];
|
|
1323
|
+
for (const root of roots) {
|
|
1324
|
+
let callers = [];
|
|
1325
|
+
try {
|
|
1326
|
+
callers = cg.getCallers(root.id);
|
|
1327
|
+
}
|
|
1328
|
+
catch { /* skip this root */ }
|
|
1329
|
+
const seen = new Set();
|
|
1330
|
+
const uniq = [];
|
|
1331
|
+
for (const c of callers) {
|
|
1332
|
+
if (c?.node && !seen.has(c.node.id)) {
|
|
1333
|
+
seen.add(c.node.id);
|
|
1334
|
+
uniq.push(c.node);
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
if (uniq.length === 0)
|
|
1338
|
+
continue; // no blast radius → nothing to flag
|
|
1339
|
+
const callerFiles = [...new Set(uniq.map((n) => rel(n.filePath)))];
|
|
1340
|
+
const testFiles = callerFiles.filter((f) => (0, query_utils_1.isTestFile)(f));
|
|
1341
|
+
const nonTest = callerFiles.filter((f) => !(0, query_utils_1.isTestFile)(f));
|
|
1342
|
+
const shown = nonTest.slice(0, FILE_CAP).map((f) => `\`${f}\``).join(', ');
|
|
1343
|
+
const more = nonTest.length > FILE_CAP ? ` +${nonTest.length - FILE_CAP} more` : '';
|
|
1344
|
+
const where = nonTest.length > 0 ? ` in ${shown}${more}` : '';
|
|
1345
|
+
const tests = testFiles.length > 0
|
|
1346
|
+
? `; tests: ${testFiles.slice(0, FILE_CAP).map((f) => `\`${f}\``).join(', ')}${testFiles.length > FILE_CAP ? ` +${testFiles.length - FILE_CAP}` : ''}`
|
|
1347
|
+
: '; ⚠️ no covering tests found';
|
|
1348
|
+
entries.push(`- \`${root.name}\` (${rel(root.filePath)}:${root.startLine}) — ${uniq.length} caller${uniq.length === 1 ? '' : 's'}${where}${tests}`);
|
|
1349
|
+
}
|
|
1350
|
+
if (entries.length === 0)
|
|
1351
|
+
return '';
|
|
1352
|
+
return [
|
|
1353
|
+
'### Blast radius — what depends on these (update/verify before editing)',
|
|
1354
|
+
'',
|
|
1355
|
+
...entries,
|
|
1356
|
+
'',
|
|
1357
|
+
].join('\n');
|
|
1358
|
+
}
|
|
1359
|
+
/**
|
|
1360
|
+
* Graph-connectivity relevance via Random-Walk-with-Restart (personalized
|
|
1361
|
+
* PageRank) from the query's matched SEED nodes over the call/reference graph.
|
|
1362
|
+
*
|
|
1363
|
+
* This is the ranking signal text search (FTS/bm25) CANNOT provide, and it's
|
|
1364
|
+
* codegraph's home turf: relevance by STRUCTURE, not words. A file whose
|
|
1365
|
+
* symbols are call-connected to the matched cluster accrues walk mass and
|
|
1366
|
+
* ranks high; a lone TEXT match — e.g. `LensSwitcher.swift` matched the word
|
|
1367
|
+
* "switch" from `switchOrganization`, but calls none of `setUser`/`fetchUser`
|
|
1368
|
+
* — gets only its own restart probability and ranks ~0. Immune to the
|
|
1369
|
+
* tokenization trap that fools term matching, deterministic, no embeddings.
|
|
1370
|
+
*
|
|
1371
|
+
* Undirected adjacency (reachability both ways), restart α=0.25 to the seeds,
|
|
1372
|
+
* power iteration to convergence. Bounded to the already-relevant subgraph, so
|
|
1373
|
+
* it's a few hundred nodes × ~25 iterations — negligible cost.
|
|
1374
|
+
*/
|
|
1375
|
+
computeGraphRelevance(nodeIds, edges, seedIds) {
|
|
1376
|
+
const out = new Map();
|
|
1377
|
+
const n = nodeIds.length;
|
|
1378
|
+
if (n === 0)
|
|
1379
|
+
return out;
|
|
1380
|
+
const idx = new Map();
|
|
1381
|
+
for (let i = 0; i < n; i++)
|
|
1382
|
+
idx.set(nodeIds[i], i);
|
|
1383
|
+
const RANK_EDGES = new Set([
|
|
1384
|
+
'calls', 'references', 'extends', 'implements', 'overrides',
|
|
1385
|
+
'instantiates', 'returns', 'type_of', 'imports',
|
|
1386
|
+
]);
|
|
1387
|
+
const adj = Array.from({ length: n }, () => []);
|
|
1388
|
+
for (const e of edges) {
|
|
1389
|
+
if (!RANK_EDGES.has(e.kind))
|
|
1390
|
+
continue;
|
|
1391
|
+
const i = idx.get(e.source);
|
|
1392
|
+
const j = idx.get(e.target);
|
|
1393
|
+
if (i === undefined || j === undefined || i === j)
|
|
1394
|
+
continue;
|
|
1395
|
+
adj[i].push(j);
|
|
1396
|
+
adj[j].push(i); // undirected — reachable either direction
|
|
1397
|
+
}
|
|
1398
|
+
// Restart vector: uniform over seeds present in the candidate set. (Falls
|
|
1399
|
+
// back to uniform-over-all if no seed landed in the set, so we never return
|
|
1400
|
+
// all-zero.)
|
|
1401
|
+
const r = new Array(n).fill(0);
|
|
1402
|
+
let rsum = 0;
|
|
1403
|
+
for (const id of seedIds) {
|
|
1404
|
+
const i = idx.get(id);
|
|
1405
|
+
if (i !== undefined) {
|
|
1406
|
+
r[i] = 1;
|
|
1407
|
+
rsum += 1;
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
if (rsum === 0) {
|
|
1411
|
+
for (let i = 0; i < n; i++)
|
|
1412
|
+
r[i] = 1;
|
|
1413
|
+
rsum = n;
|
|
1414
|
+
}
|
|
1415
|
+
for (let i = 0; i < n; i++)
|
|
1416
|
+
r[i] /= rsum;
|
|
1417
|
+
const alpha = 0.25;
|
|
1418
|
+
let s = r.slice();
|
|
1419
|
+
for (let iter = 0; iter < 25; iter++) {
|
|
1420
|
+
const next = new Array(n).fill(0);
|
|
1421
|
+
for (let i = 0; i < n; i++) {
|
|
1422
|
+
const si = s[i];
|
|
1423
|
+
if (si === 0)
|
|
1424
|
+
continue;
|
|
1425
|
+
const d = adj[i].length;
|
|
1426
|
+
if (d === 0) {
|
|
1427
|
+
next[i] += si;
|
|
1428
|
+
continue;
|
|
1429
|
+
} // dangling: keep its mass
|
|
1430
|
+
const share = si / d;
|
|
1431
|
+
for (const j of adj[i])
|
|
1432
|
+
next[j] += share;
|
|
1433
|
+
}
|
|
1434
|
+
for (let i = 0; i < n; i++)
|
|
1435
|
+
s[i] = (1 - alpha) * next[i] + alpha * r[i];
|
|
1436
|
+
}
|
|
1437
|
+
for (let i = 0; i < n; i++)
|
|
1438
|
+
out.set(nodeIds[i], s[i]);
|
|
1439
|
+
return out;
|
|
1440
|
+
}
|
|
2034
1441
|
/**
|
|
2035
1442
|
* Handle codegraph_explore — deep exploration in a single call
|
|
2036
1443
|
*
|
|
@@ -2127,18 +1534,50 @@ class ToolHandler {
|
|
|
2127
1534
|
const tokens = [...new Set(query.split(/[\s,()[\]]+/)
|
|
2128
1535
|
.map((t) => t.replace(FILE_EXT, '').trim())
|
|
2129
1536
|
.filter((t) => t.length >= 3 && /^[A-Za-z_$][\w$]*(?:(?:::|\.)[\w$]+)*$/.test(t)))].slice(0, 16);
|
|
1537
|
+
// PascalCase tokens in the query are type/file disambiguators — when the
|
|
1538
|
+
// agent writes "DataRequest task validate", the `task`/`validate` it wants
|
|
1539
|
+
// are DataRequest's, NOT the same-named overloads in Validation.swift /
|
|
1540
|
+
// Concurrency.swift / the abstract base. Used below to bias overloaded
|
|
1541
|
+
// names toward the file/class the query also names.
|
|
1542
|
+
const typeTokens = tokens.filter((o) => /^[A-Z][A-Za-z0-9]{3,}/.test(o));
|
|
1543
|
+
const inNamedContext = (n) => typeTokens.some((ct) => {
|
|
1544
|
+
const lc = ct.toLowerCase();
|
|
1545
|
+
return n.filePath.toLowerCase().includes(lc) || n.qualifiedName.toLowerCase().includes(lc);
|
|
1546
|
+
});
|
|
2130
1547
|
for (const t of tokens) {
|
|
2131
|
-
|
|
1548
|
+
// Enumerate ALL defs of a bare token via the direct index, not FTS — a
|
|
1549
|
+
// 50+-overload name (tokio `poll`) ranks the wanted def (`Harness::poll`)
|
|
1550
|
+
// below the FTS cut, so findAllSymbols would never see it and the
|
|
1551
|
+
// type-token bias below couldn't pick the harness.rs one. (Same fix as
|
|
1552
|
+
// codegraph_node's findSymbolMatches.) Qualified tokens keep findAllSymbols.
|
|
1553
|
+
const isQual = /[.\/]|::/.test(t);
|
|
1554
|
+
const raw = isQual ? this.findAllSymbols(cg, t).nodes : cg.getNodesByName(t);
|
|
1555
|
+
const cands = raw
|
|
2132
1556
|
.filter((n) => CALLABLE.has(n.kind) && !isTestPath(n.filePath))
|
|
2133
1557
|
.sort((a, b) => (bodyLines(b) > 1 ? 1 : 0) - (bodyLines(a) > 1 ? 1 : 0) || bodyLines(b) - bodyLines(a));
|
|
2134
|
-
// A specific name (<=3 defs) injects all its defs
|
|
2135
|
-
// (`
|
|
2136
|
-
//
|
|
2137
|
-
|
|
2138
|
-
|
|
1558
|
+
// A specific name (<=3 defs) injects all its defs. An overloaded name
|
|
1559
|
+
// (`validate` = 10, `request` = 44) would flood the subgraph, so inject
|
|
1560
|
+
// only: the overloads whose file/class the query ALSO names (the agent
|
|
1561
|
+
// told us which one it wants — DataRequest's, not Validation.swift's),
|
|
1562
|
+
// capped; else fall back to the single most-substantive def. This is the
|
|
1563
|
+
// explore-side mirror of codegraph_node's overload disambiguation.
|
|
1564
|
+
let picks;
|
|
1565
|
+
if (cands.length <= 3) {
|
|
1566
|
+
picks = cands;
|
|
1567
|
+
}
|
|
1568
|
+
else {
|
|
1569
|
+
const ctx = cands.filter(inNamedContext);
|
|
1570
|
+
picks = ctx.length > 0 ? ctx.slice(0, 4) : cands.slice(0, 1);
|
|
1571
|
+
}
|
|
1572
|
+
for (const n of picks) {
|
|
1573
|
+
if (!subgraph.nodes.has(n.id))
|
|
2139
1574
|
subgraph.nodes.set(n.id, n);
|
|
2140
|
-
|
|
2141
|
-
|
|
1575
|
+
// Mark as a named seed EVEN IF the FTS gather already had it — being
|
|
1576
|
+
// "named by the agent" is independent of whether search happened to
|
|
1577
|
+
// surface it, and it drives the +50 score, the gate, and the
|
|
1578
|
+
// named-file sort below. (Previously only NEW injections were marked,
|
|
1579
|
+
// so a named symbol FTS already gathered never sorted to the top.)
|
|
1580
|
+
namedSeedIds.add(n.id);
|
|
2142
1581
|
}
|
|
2143
1582
|
}
|
|
2144
1583
|
}
|
|
@@ -2218,21 +1657,107 @@ class ToolHandler {
|
|
|
2218
1657
|
}
|
|
2219
1658
|
}
|
|
2220
1659
|
}
|
|
2221
|
-
//
|
|
1660
|
+
// Secondary signal: how many DISTINCT query terms each file matches (path +
|
|
1661
|
+
// symbol names). Kept only as a tiebreak — the PRIMARY relevance is graph
|
|
1662
|
+
// connectivity below. (Term counting alone tied the real central file with
|
|
1663
|
+
// incidental same-word matches; it's a weak text signal, not the ranker.)
|
|
1664
|
+
const uniqueQueryTerms = [...new Set(queryTerms)].filter(t => t.length >= 3);
|
|
1665
|
+
const fileTermHits = new Map();
|
|
1666
|
+
for (const [fp, group] of relevantFiles) {
|
|
1667
|
+
const hay = fp.toLowerCase() + ' ' + group.nodes.map(n => n.name.toLowerCase()).join(' ');
|
|
1668
|
+
let hits = 0;
|
|
1669
|
+
for (const t of uniqueQueryTerms)
|
|
1670
|
+
if (hay.includes(t))
|
|
1671
|
+
hits++;
|
|
1672
|
+
fileTermHits.set(fp, hits);
|
|
1673
|
+
}
|
|
1674
|
+
// PRIMARY relevance: graph connectivity (Random-Walk-with-Restart from the
|
|
1675
|
+
// matched seeds — see computeGraphRelevance). Aggregate each file's nodes'
|
|
1676
|
+
// walk mass. This is the signal text search lacks: the real cluster
|
|
1677
|
+
// (org-user.storage.ts, call-connected to the matches) accrues mass; a lone
|
|
1678
|
+
// text match (LensSwitcher.swift, matched "switch" but calls nothing in the
|
|
1679
|
+
// flow) gets only its restart probability → ~0, and is dropped by the gate.
|
|
1680
|
+
const nodeRwr = this.computeGraphRelevance([...subgraph.nodes.keys()], subgraph.edges, entryNodeIds);
|
|
1681
|
+
const fileGraphScore = new Map();
|
|
1682
|
+
for (const node of subgraph.nodes.values()) {
|
|
1683
|
+
fileGraphScore.set(node.filePath, (fileGraphScore.get(node.filePath) ?? 0) + (nodeRwr.get(node.id) ?? 0));
|
|
1684
|
+
}
|
|
1685
|
+
const maxGraph = Math.max(0, ...fileGraphScore.values());
|
|
1686
|
+
// Central file(s): the 1-2 most graph-central files that also match the
|
|
1687
|
+
// query textually (so a connected hub-utility with no term match isn't
|
|
1688
|
+
// mistaken for the subject). The heart of the answer — they earn the larger
|
|
1689
|
+
// WHOLE-FILE ceiling below (a god-file central file still exceeds it and
|
|
1690
|
+
// falls to generous full-method sectioning — never a whole dump).
|
|
1691
|
+
const centralFiles = new Set([...fileGraphScore.entries()]
|
|
1692
|
+
.filter(([fp, g]) => g > 0 && (fileTermHits.get(fp) ?? 0) >= 1)
|
|
1693
|
+
.sort((a, b) => b[1] - a[1] || (fileTermHits.get(b[0]) ?? 0) - (fileTermHits.get(a[0]) ?? 0))
|
|
1694
|
+
.slice(0, 2)
|
|
1695
|
+
.map(([f]) => f));
|
|
1696
|
+
// Files that DEFINE a symbol the agent named (or a subgraph root). These are
|
|
1697
|
+
// the highest-relevance files there are — the agent asked for them by name —
|
|
1698
|
+
// so the connectivity gate below must never drop them, even when their RWR
|
|
1699
|
+
// mass is low (a leaf family file like codec.ts is call-connected to little
|
|
1700
|
+
// but is exactly what the agent queried). Without this protection the gate
|
|
1701
|
+
// prunes a named file and the agent Reads it back.
|
|
1702
|
+
const entryFiles = new Set();
|
|
1703
|
+
for (const id of entryNodeIds) {
|
|
1704
|
+
const n = subgraph.nodes.get(id);
|
|
1705
|
+
if (n)
|
|
1706
|
+
entryFiles.add(n.filePath);
|
|
1707
|
+
}
|
|
1708
|
+
// Relevance gate (so the generous budget is a CEILING, not a target): keep a
|
|
1709
|
+
// file only if it is STRUCTURALLY relevant by ANY of:
|
|
1710
|
+
// - graph score within a fraction of the top (it's on/near the flow), OR
|
|
1711
|
+
// - central (a query entry-point lives here), OR
|
|
1712
|
+
// - it DEFINES a symbol the agent named (entryFiles), OR
|
|
1713
|
+
// - it matches >= 2 DISTINCT named query terms — a strong text signal that
|
|
1714
|
+
// the agent is asking about this file even when nothing calls it (codec.ts:
|
|
1715
|
+
// the agent named `encode`/`Codec`/`JsonCodec`, all leaf classes with zero
|
|
1716
|
+
// RWR mass — graph alone wrongly drops it).
|
|
1717
|
+
// A lone text match on one shared word (LensSwitcher: term=1, g~0) is still
|
|
1718
|
+
// dropped, so the budget never fills with incidental files. Guarded so it
|
|
1719
|
+
// never prunes below 2.
|
|
1720
|
+
if (maxGraph > 0) {
|
|
1721
|
+
const gated = relevantFiles.filter(([fp]) => (fileGraphScore.get(fp) ?? 0) >= maxGraph * 0.06
|
|
1722
|
+
|| centralFiles.has(fp)
|
|
1723
|
+
|| entryFiles.has(fp)
|
|
1724
|
+
|| (fileTermHits.get(fp) ?? 0) >= 2);
|
|
1725
|
+
if (gated.length >= 2)
|
|
1726
|
+
relevantFiles = gated;
|
|
1727
|
+
}
|
|
1728
|
+
// Sort files: graph-central first, then distinct-term match, then the
|
|
1729
|
+
// existing low-value/generated/score tiebreaks.
|
|
1730
|
+
// Files that DEFINE a symbol the agent NAMED. These sort first — ahead of
|
|
1731
|
+
// graph connectivity — because the agent asked for them by name. Without
|
|
1732
|
+
// this, a named leaf override reached only by dynamic dispatch (Alamofire's
|
|
1733
|
+
// `DataRequest.task`/`validate`, low RWR mass) sorts below the high-
|
|
1734
|
+
// connectivity abstract base (`Request.swift`) and the same-named overloads
|
|
1735
|
+
// in other files (`Validation.swift`), falls outside the budget, and the
|
|
1736
|
+
// agent Reads it. The named file is the answer — rank it at the top.
|
|
1737
|
+
const namedSeedFiles = new Set();
|
|
1738
|
+
for (const id of namedSeedIds) {
|
|
1739
|
+
const n = subgraph.nodes.get(id);
|
|
1740
|
+
if (n)
|
|
1741
|
+
namedSeedFiles.add(n.filePath);
|
|
1742
|
+
}
|
|
2222
1743
|
const sortedFiles = relevantFiles.sort((a, b) => {
|
|
2223
1744
|
const aPath = a[0].toLowerCase();
|
|
2224
1745
|
const bPath = b[0].toLowerCase();
|
|
2225
|
-
//
|
|
2226
|
-
const
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
const
|
|
2233
|
-
const
|
|
2234
|
-
if (
|
|
2235
|
-
return
|
|
1746
|
+
// Agent-named files first (it asked for a symbol defined here by name).
|
|
1747
|
+
const aNamed = namedSeedFiles.has(a[0]) ? 1 : 0;
|
|
1748
|
+
const bNamed = namedSeedFiles.has(b[0]) ? 1 : 0;
|
|
1749
|
+
if (aNamed !== bNamed)
|
|
1750
|
+
return bNamed - aNamed;
|
|
1751
|
+
// Graph connectivity is the next key (small epsilon so near-ties fall
|
|
1752
|
+
// through to the text signal rather than coin-flipping on float noise).
|
|
1753
|
+
const aG = fileGraphScore.get(a[0]) ?? 0;
|
|
1754
|
+
const bG = fileGraphScore.get(b[0]) ?? 0;
|
|
1755
|
+
if (Math.abs(aG - bG) > maxGraph * 0.01)
|
|
1756
|
+
return bG - aG;
|
|
1757
|
+
const aHits = fileTermHits.get(a[0]) ?? 0;
|
|
1758
|
+
const bHits = fileTermHits.get(b[0]) ?? 0;
|
|
1759
|
+
if (aHits !== bHits)
|
|
1760
|
+
return bHits - aHits;
|
|
2236
1761
|
const aLow = isLowValue(aPath);
|
|
2237
1762
|
const bLow = isLowValue(bPath);
|
|
2238
1763
|
if (aLow !== bLow)
|
|
@@ -2258,6 +1783,12 @@ class ToolHandler {
|
|
|
2258
1783
|
`Found ${subgraph.nodes.size} symbols across ${fileGroups.size} files.`,
|
|
2259
1784
|
'',
|
|
2260
1785
|
];
|
|
1786
|
+
// Blast radius (always-on, compact): for the entry symbols, who depends on
|
|
1787
|
+
// them + which tests cover them — locations only, no source — so the agent
|
|
1788
|
+
// knows what to update/verify before editing without a separate call.
|
|
1789
|
+
const blastRadius = this.buildBlastRadiusSection(cg, subgraph);
|
|
1790
|
+
if (blastRadius)
|
|
1791
|
+
lines.push(blastRadius);
|
|
2261
1792
|
// Relationship map — show how symbols connect
|
|
2262
1793
|
const significantEdges = subgraph.edges.filter(e => e.kind !== 'contains' // skip contains — it's implied by file grouping
|
|
2263
1794
|
);
|
|
@@ -2412,7 +1943,7 @@ class ToolHandler {
|
|
|
2412
1943
|
// so leave it to the normal full render.
|
|
2413
1944
|
const namedBodyChars = group.nodes
|
|
2414
1945
|
.filter(n => CALLABLE_BODY.has(n.kind) && (flow.pathNodeIds.has(n.id) || flow.uniqueNamedNodeIds.has(n.id)))
|
|
2415
|
-
.reduce((s, n) => s + fileLines.slice(n.startLine - 1,
|
|
1946
|
+
.reduce((s, n) => s + fileLines.slice(n.startLine - 1, n.endLine).join('\n').length, 0);
|
|
2416
1947
|
const onSpineGodFile = hasSpineNode
|
|
2417
1948
|
&& namedBodyChars > budget.maxCharsPerFile
|
|
2418
1949
|
&& group.nodes.some(n => CALLABLE_BODY.has(n.kind) && flow.uniqueNamedNodeIds.has(n.id) && !flow.pathNodeIds.has(n.id));
|
|
@@ -2432,14 +1963,19 @@ class ToolHandler {
|
|
|
2432
1963
|
: flow.pathNodeIds.has(n.id) ? 0
|
|
2433
1964
|
: flow.uniqueNamedNodeIds.has(n.id) ? 1
|
|
2434
1965
|
: (fileDefinesSuper && flow.namedNodeIds.has(n.id)) ? 2 : 99;
|
|
2435
|
-
|
|
1966
|
+
// One ~250-line WINDOW per file. syms are taken by priority (spine first,
|
|
1967
|
+
// then uniquely-named, then family-base), and the cap applies to ALL of
|
|
1968
|
+
// them — including the spine — so a big-spine god-file (tokio's worker.rs:
|
|
1969
|
+
// run→run_task→next_task→steal_work) can't eat the whole response and
|
|
1970
|
+
// starve the co-flow file (harness.rs's poll). The native agent windows
|
|
1971
|
+
// such a file too (~190 lines at a time), so this mimics, not truncates.
|
|
1972
|
+
// Always emit ≥1 (never an empty section).
|
|
1973
|
+
const bodyCap = budget.maxCharsPerFile * 1.5;
|
|
2436
1974
|
const bodyIds = new Set();
|
|
2437
1975
|
let bodyChars = 0;
|
|
2438
1976
|
for (const n of syms.filter(n => prio(n) < 99 && n.endLine >= n.startLine).sort((a, b) => prio(a) - prio(b))) {
|
|
2439
|
-
const sz = fileLines.slice(n.startLine - 1,
|
|
2440
|
-
|
|
2441
|
-
// off-path extras (unique-named, family base), never the flow path itself.
|
|
2442
|
-
if (prio(n) > 0 && bodyChars + sz > bodyCap && bodyIds.size > 0)
|
|
1977
|
+
const sz = fileLines.slice(n.startLine - 1, n.endLine).join('\n').length;
|
|
1978
|
+
if (bodyChars + sz > bodyCap && bodyIds.size > 0)
|
|
2443
1979
|
continue;
|
|
2444
1980
|
bodyIds.add(n.id);
|
|
2445
1981
|
bodyChars += sz;
|
|
@@ -2455,7 +1991,7 @@ class ToolHandler {
|
|
|
2455
1991
|
if (n.startLine <= coveredUntil)
|
|
2456
1992
|
continue;
|
|
2457
1993
|
if (bodyIds.has(n.id)) {
|
|
2458
|
-
const end =
|
|
1994
|
+
const end = n.endLine;
|
|
2459
1995
|
const body = fileLines.slice(n.startLine - 1, end).join('\n');
|
|
2460
1996
|
skel.push(exploreLineNumbersEnabled() ? numberSourceLines(body, n.startLine) : body);
|
|
2461
1997
|
coveredUntil = end;
|
|
@@ -2503,14 +2039,30 @@ class ToolHandler {
|
|
|
2503
2039
|
continue;
|
|
2504
2040
|
}
|
|
2505
2041
|
}
|
|
2506
|
-
// Whole-
|
|
2507
|
-
//
|
|
2508
|
-
//
|
|
2509
|
-
//
|
|
2510
|
-
//
|
|
2042
|
+
// Whole-file rule: if a relevant file is small enough to afford, return it
|
|
2043
|
+
// ENTIRELY instead of clustering. Clustering exists to tame god-files
|
|
2044
|
+
// (App.tsx ~13k lines); on a ~134-line component a cluster is a lossy
|
|
2045
|
+
// subset of a file the agent will just Read in full anyway — costing a
|
|
2046
|
+
// round-trip and a re-read every later turn. Reserve clustering for files
|
|
2511
2047
|
// too big to ship whole. Still bounded by the total maxOutputChars check.
|
|
2512
|
-
|
|
2513
|
-
|
|
2048
|
+
//
|
|
2049
|
+
// CENTRAL files (where the query's entry points live) get a larger — but
|
|
2050
|
+
// bounded — ceiling: they're the heart of the answer, the file(s) the agent
|
|
2051
|
+
// would Read whole, so a genuinely small one comes back whole rather than as
|
|
2052
|
+
// thin clusters. A LARGE central file (the 791-line org-user store) exceeds
|
|
2053
|
+
// the ceiling and falls through to sectioning/clustering below — full method
|
|
2054
|
+
// bodies + signatures — so we never dump (or overflow on) a whole god-file.
|
|
2055
|
+
const isCentralFile = centralFiles.has(filePath);
|
|
2056
|
+
// Central files get a slightly larger whole-file window than peripheral ones,
|
|
2057
|
+
// but a TIGHT one (~1.5× the per-file cap): the native read of a central file
|
|
2058
|
+
// is a ~150–250 line orientation window, NOT the whole file. A flat "whole
|
|
2059
|
+
// central file" both overflowed the inline cap AND starved the co-flow files
|
|
2060
|
+
// (worker.rs ate the budget, dropping harness.rs's poll). A larger central
|
|
2061
|
+
// file falls through to per-method windowing/clustering below.
|
|
2062
|
+
const WHOLE_FILE_MAX_LINES = isCentralFile ? 280 : 220;
|
|
2063
|
+
const WHOLE_FILE_MAX_CHARS = isCentralFile
|
|
2064
|
+
? Math.min(Math.max(0, budget.maxOutputChars - totalChars - 200), Math.round(budget.maxCharsPerFile * 1.5))
|
|
2065
|
+
: budget.maxCharsPerFile * 3;
|
|
2514
2066
|
if (fileLines.length <= WHOLE_FILE_MAX_LINES && fileContent.length <= WHOLE_FILE_MAX_CHARS) {
|
|
2515
2067
|
const body = fileContent.replace(/\n+$/, '');
|
|
2516
2068
|
let wholeSection = exploreLineNumbersEnabled() ? numberSourceLines(body, 1) : body;
|
|
@@ -2520,12 +2072,12 @@ class ToolHandler {
|
|
|
2520
2072
|
const headerNames = uniqSymbols.slice(0, budget.maxSymbolsInFileHeader);
|
|
2521
2073
|
const omitted = uniqSymbols.length - headerNames.length;
|
|
2522
2074
|
const wholeHeader = `#### ${filePath} — ${omitted > 0 ? `${headerNames.join(', ')}, +${omitted} more` : headerNames.join(', ')}`;
|
|
2523
|
-
if (totalChars + wholeSection.length + 200 > budget.maxOutputChars) {
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
wholeSection = wholeSection.slice(0, remaining) + '\n... (trimmed) ...';
|
|
2075
|
+
if (!fileNecessary && totalChars + wholeSection.length + 200 > budget.maxOutputChars) {
|
|
2076
|
+
// Don't slice a whole file mid-method: an incidental file that doesn't
|
|
2077
|
+
// fit is skipped; a necessary one (below) renders in full. Half a file
|
|
2078
|
+
// forces the Read this is meant to prevent.
|
|
2528
2079
|
anyFileTrimmed = true;
|
|
2080
|
+
continue;
|
|
2529
2081
|
}
|
|
2530
2082
|
lines.push(wholeHeader, '', '```' + lang, wholeSection, '```', '');
|
|
2531
2083
|
totalChars += wholeSection.length + 200;
|
|
@@ -2706,7 +2258,6 @@ class ToolHandler {
|
|
|
2706
2258
|
// Emit chosen clusters in source order so the file reads top-to-bottom.
|
|
2707
2259
|
let fileSection = '';
|
|
2708
2260
|
const allSymbols = [];
|
|
2709
|
-
let fileTrimmed = false;
|
|
2710
2261
|
for (let i = 0; i < clusters.length; i++) {
|
|
2711
2262
|
if (!chosenIndices.has(i))
|
|
2712
2263
|
continue;
|
|
@@ -2717,13 +2268,12 @@ class ToolHandler {
|
|
|
2717
2268
|
fileSection += section;
|
|
2718
2269
|
allSymbols.push(...cluster.symbols);
|
|
2719
2270
|
}
|
|
2720
|
-
//
|
|
2721
|
-
//
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
if (chosenIndices.size < clusters.length || fileTrimmed) {
|
|
2271
|
+
// A chosen cluster is a COMPLETE method-range — we never cut through a body.
|
|
2272
|
+
// An oversize single cluster (a long monolithic function) renders in FULL:
|
|
2273
|
+
// half a method is useless (the agent just Reads the rest for the other half),
|
|
2274
|
+
// which is the very fallback explore exists to prevent. A pathological file is
|
|
2275
|
+
// bounded by the per-file cluster SELECTION above + the total hard ceiling.
|
|
2276
|
+
if (chosenIndices.size < clusters.length) {
|
|
2727
2277
|
anyFileTrimmed = true;
|
|
2728
2278
|
}
|
|
2729
2279
|
// Dedupe + cap the symbols list shown in the per-file header. Some
|
|
@@ -2755,11 +2305,11 @@ class ToolHandler {
|
|
|
2755
2305
|
// (DataRequest/Validation) all render, instead of the cap dropping whichever
|
|
2756
2306
|
// phase the file order happened to put last.
|
|
2757
2307
|
if (!fileNecessary && totalChars + fileSection.length + 200 > budget.maxOutputChars) {
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
fileSection = fileSection.slice(0, remaining) + '\n... (trimmed) ...';
|
|
2308
|
+
// Incidental file that doesn't fit: SKIP it whole — never slice mid-method.
|
|
2309
|
+
// Keep scanning for necessary files (which bypass this cap and render in
|
|
2310
|
+
// full, bounded by the hard ceiling).
|
|
2762
2311
|
anyFileTrimmed = true;
|
|
2312
|
+
continue;
|
|
2763
2313
|
}
|
|
2764
2314
|
lines.push(fileHeader);
|
|
2765
2315
|
lines.push('');
|
|
@@ -2816,26 +2366,26 @@ class ToolHandler {
|
|
|
2816
2366
|
// Stats unavailable — skip budget note
|
|
2817
2367
|
}
|
|
2818
2368
|
}
|
|
2819
|
-
//
|
|
2820
|
-
//
|
|
2821
|
-
//
|
|
2822
|
-
//
|
|
2823
|
-
//
|
|
2824
|
-
//
|
|
2825
|
-
//
|
|
2826
|
-
//
|
|
2827
|
-
// only incidental ones, all bounded by maxFiles + per-file true-spine — so
|
|
2828
|
-
// this is a SAFETY ceiling above that necessary content, not a hard cut
|
|
2829
|
-
// through it. Cutting at a flat maxOutputChars here undid the whole point:
|
|
2830
|
-
// Alamofire's loop assembles build+validators-exec+validate (~15K) and a 13K
|
|
2831
|
-
// slice dropped the validate phase the agent then Read. Allow necessary
|
|
2832
|
-
// overflow up to 1.5× (still bounds a pathological monolith).
|
|
2369
|
+
// Final ceiling — an ABSOLUTE inline cap, not a multiple of the budget. The
|
|
2370
|
+
// render loop renders necessary (named/spine) files even a bit past
|
|
2371
|
+
// maxOutputChars and caps only incidental ones, so this is the last safety.
|
|
2372
|
+
// It MUST stay under the host's inline tool-result limit (~25K chars): above
|
|
2373
|
+
// that the result is externalized to a file the agent Reads back (a 35K
|
|
2374
|
+
// vscode explore did exactly this in the n=4 A/B). So allow a little
|
|
2375
|
+
// necessary overflow above the 24K budget, but hard-stop at 25K — never into
|
|
2376
|
+
// externalize territory.
|
|
2833
2377
|
const output = flow.text + lines.join('\n');
|
|
2834
|
-
const hardCeiling = Math.round(budget.maxOutputChars * 1.5);
|
|
2378
|
+
const hardCeiling = Math.min(Math.round(budget.maxOutputChars * 1.5), 25000);
|
|
2835
2379
|
if (output.length > hardCeiling) {
|
|
2380
|
+
// Cut at a FILE-SECTION boundary (the last `#### ` header before the
|
|
2381
|
+
// ceiling) so we drop whole trailing file-sections rather than slicing
|
|
2382
|
+
// through a method body — a half-rendered method just forces the Read this
|
|
2383
|
+
// tool exists to prevent. Fall back to a line boundary only if no section
|
|
2384
|
+
// header sits in the back half (degenerate single-giant-section case).
|
|
2836
2385
|
const cut = output.slice(0, hardCeiling);
|
|
2837
|
-
const
|
|
2838
|
-
const
|
|
2386
|
+
const lastSection = cut.lastIndexOf('\n#### ');
|
|
2387
|
+
const boundary = lastSection > hardCeiling * 0.5 ? lastSection : cut.lastIndexOf('\n');
|
|
2388
|
+
const safe = boundary > 0 ? cut.slice(0, boundary) : cut;
|
|
2839
2389
|
return this.textResult(safe + '\n\n... (output truncated to budget; the source above is complete and verbatim — treat it as already Read. For any area not covered, run another codegraph_explore with the specific names — do NOT Read these files.)');
|
|
2840
2390
|
}
|
|
2841
2391
|
return this.textResult(output);
|
|
@@ -2850,29 +2400,110 @@ class ToolHandler {
|
|
|
2850
2400
|
const cg = this.getCodeGraph(args.projectPath);
|
|
2851
2401
|
// Default to false to minimize context usage
|
|
2852
2402
|
const includeCode = args.includeCode === true;
|
|
2853
|
-
const
|
|
2854
|
-
|
|
2403
|
+
const fileHint = typeof args.file === 'string' && args.file.trim() ? args.file.trim() : undefined;
|
|
2404
|
+
const lineHint = typeof args.line === 'number' && args.line > 0 ? args.line : undefined;
|
|
2405
|
+
let matches = this.findSymbolMatches(cg, symbol);
|
|
2406
|
+
if (matches.length === 0) {
|
|
2855
2407
|
return this.textResult(`Symbol "${symbol}" not found in the codebase`);
|
|
2856
2408
|
}
|
|
2409
|
+
// Disambiguate a heavily-overloaded name to a specific definition the caller
|
|
2410
|
+
// pinned by file/line (the `file:line` a trail or another tool showed it) —
|
|
2411
|
+
// so it can fetch e.g. `Harness::poll` at harness.rs:153 out of 50+ `poll`s
|
|
2412
|
+
// instead of Reading. file matches by path suffix/substring; line prefers the
|
|
2413
|
+
// def whose body contains it, else the nearest start. Only narrows (never
|
|
2414
|
+
// empties — if a hint matches nothing it's ignored).
|
|
2415
|
+
if (matches.length > 1 && (fileHint || lineHint !== undefined)) {
|
|
2416
|
+
const norm = (p) => p.replace(/\\/g, '/').toLowerCase();
|
|
2417
|
+
let narrowed = matches;
|
|
2418
|
+
if (fileHint) {
|
|
2419
|
+
const fh = norm(fileHint);
|
|
2420
|
+
const byFile = narrowed.filter((n) => norm(n.filePath).endsWith(fh) || norm(n.filePath).includes(fh));
|
|
2421
|
+
if (byFile.length > 0)
|
|
2422
|
+
narrowed = byFile;
|
|
2423
|
+
}
|
|
2424
|
+
if (lineHint !== undefined && narrowed.length > 1) {
|
|
2425
|
+
const containing = narrowed.filter((n) => n.startLine <= lineHint && (n.endLine ?? n.startLine) >= lineHint);
|
|
2426
|
+
narrowed = containing.length > 0
|
|
2427
|
+
? containing
|
|
2428
|
+
: [...narrowed].sort((a, b) => Math.abs(a.startLine - lineHint) - Math.abs(b.startLine - lineHint)).slice(0, 1);
|
|
2429
|
+
}
|
|
2430
|
+
if (narrowed.length > 0)
|
|
2431
|
+
matches = narrowed;
|
|
2432
|
+
}
|
|
2433
|
+
// Single definition — the common case.
|
|
2434
|
+
if (matches.length === 1) {
|
|
2435
|
+
return this.textResult(this.truncateOutput(await this.renderNodeSection(cg, matches[0], includeCode)));
|
|
2436
|
+
}
|
|
2437
|
+
// Multiple definitions share this name — overloads, or same-named methods on
|
|
2438
|
+
// different types (Alamofire `didCompleteTask`/`task`/`validate`, gin
|
|
2439
|
+
// `reset`). Returning ONE forces the agent to guess, and when it guesses
|
|
2440
|
+
// wrong it READS the file to find the right overload — the dominant
|
|
2441
|
+
// codegraph_node read cause on Swift/Go. So return them ALL: pack as many
|
|
2442
|
+
// FULL bodies as fit a char budget (the agent gets the one it needs in this
|
|
2443
|
+
// one call, no follow-up parameter to learn), and list any remainder by
|
|
2444
|
+
// file:line so a large overload set can't overflow the per-tool cap.
|
|
2445
|
+
const header = `**${matches.length} definitions named "${symbol}"**`;
|
|
2446
|
+
if (!includeCode) {
|
|
2447
|
+
const list = matches.map((n) => `- \`${n.name}\` (${n.kind}) — ${n.filePath}:${n.startLine}`);
|
|
2448
|
+
return this.textResult(this.truncateOutput([header, '', 'Re-query with `includeCode: true` to get every body in one call — no need to pick one first.', '', ...list].join('\n')));
|
|
2449
|
+
}
|
|
2450
|
+
const BODY_BUDGET = 12000; // leaves room under MAX_OUTPUT_LENGTH for the header + list
|
|
2451
|
+
// The CHAR budget is the real limiter — keep the count cap high so a set of
|
|
2452
|
+
// SHORT overloads (Alamofire's 10 `validate` variants, each a few lines) all
|
|
2453
|
+
// render in full rather than relegating the one the agent wanted to a
|
|
2454
|
+
// bodiless list. Only a set of many LARGE bodies hits the char budget first.
|
|
2455
|
+
const HARD_CAP = 16;
|
|
2456
|
+
const rendered = [];
|
|
2457
|
+
const listed = [];
|
|
2458
|
+
let used = 0;
|
|
2459
|
+
for (const n of matches) {
|
|
2460
|
+
if (rendered.length >= HARD_CAP) {
|
|
2461
|
+
listed.push(n);
|
|
2462
|
+
continue;
|
|
2463
|
+
}
|
|
2464
|
+
const section = await this.renderNodeSection(cg, n, true);
|
|
2465
|
+
// Always emit the first; emit the rest only while within the char budget.
|
|
2466
|
+
if (rendered.length === 0 || used + section.length <= BODY_BUDGET) {
|
|
2467
|
+
rendered.push(section);
|
|
2468
|
+
used += section.length;
|
|
2469
|
+
}
|
|
2470
|
+
else {
|
|
2471
|
+
listed.push(n);
|
|
2472
|
+
}
|
|
2473
|
+
}
|
|
2474
|
+
const out = [
|
|
2475
|
+
header,
|
|
2476
|
+
`Returning ${rendered.length} in full${listed.length ? `; ${listed.length} more listed below` : ''} — pick the one you need (no Read required).`,
|
|
2477
|
+
'',
|
|
2478
|
+
rendered.join('\n\n---\n\n'),
|
|
2479
|
+
];
|
|
2480
|
+
if (listed.length) {
|
|
2481
|
+
const LIST_CAP = 20;
|
|
2482
|
+
const shownList = listed.slice(0, LIST_CAP);
|
|
2483
|
+
out.push('', '### Other definitions', ...shownList.map((n) => `- \`${n.name}\` (${n.kind}) — ${n.filePath}:${n.startLine}`));
|
|
2484
|
+
if (listed.length > LIST_CAP)
|
|
2485
|
+
out.push(`- … +${listed.length - LIST_CAP} more`);
|
|
2486
|
+
out.push('', `> Need one of these in full? Call codegraph_node again with \`file\` (e.g. \`"${listed[0].filePath.split('/').pop()}"\`) or \`line\` — do NOT Read it.`);
|
|
2487
|
+
}
|
|
2488
|
+
return this.textResult(this.truncateOutput(out.join('\n')));
|
|
2489
|
+
}
|
|
2490
|
+
/** Render one symbol: details + (optional) body/outline + its caller/callee trail. */
|
|
2491
|
+
async renderNodeSection(cg, node, includeCode) {
|
|
2857
2492
|
let code = null;
|
|
2858
2493
|
let outline = null;
|
|
2859
2494
|
if (includeCode) {
|
|
2860
2495
|
// For container symbols (class/interface/struct/…), the full body is the
|
|
2861
|
-
// sum of every method body — a wall of source
|
|
2862
|
-
//
|
|
2863
|
-
//
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
if (CONTAINER_NODE_KINDS.has(match.node.kind)) {
|
|
2867
|
-
outline = this.buildContainerOutline(cg, match.node);
|
|
2496
|
+
// sum of every method body — a wall of source. Return a structural outline
|
|
2497
|
+
// (members + signatures + line numbers) instead; leaf symbols return their
|
|
2498
|
+
// full body.
|
|
2499
|
+
if (CONTAINER_NODE_KINDS.has(node.kind)) {
|
|
2500
|
+
outline = this.buildContainerOutline(cg, node);
|
|
2868
2501
|
}
|
|
2869
2502
|
if (!outline) {
|
|
2870
|
-
code = await cg.getCode(
|
|
2503
|
+
code = await cg.getCode(node.id);
|
|
2871
2504
|
}
|
|
2872
2505
|
}
|
|
2873
|
-
|
|
2874
|
-
const formatted = this.formatNodeDetails(match.node, code, outline) + trail + match.note;
|
|
2875
|
-
return this.textResult(this.truncateOutput(formatted));
|
|
2506
|
+
return this.formatNodeDetails(node, code, outline) + this.formatTrail(cg, node);
|
|
2876
2507
|
}
|
|
2877
2508
|
/**
|
|
2878
2509
|
* Build the "trail" for a symbol: its direct callees (what it calls) and
|
|
@@ -3212,51 +2843,55 @@ class ToolHandler {
|
|
|
3212
2843
|
const segments = node.filePath.split('/').filter((s) => s.length > 0);
|
|
3213
2844
|
return containerHints.every((hint) => segments.some((seg) => seg === hint || seg.replace(/\.[^.]+$/, '') === hint));
|
|
3214
2845
|
}
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
2846
|
+
/**
|
|
2847
|
+
* Find ALL definitions matching a name, ranked, so codegraph_node can return
|
|
2848
|
+
* every overload instead of guessing one (the wrong guess → a Read). Keepers
|
|
2849
|
+
* rank before generated stubs (.pb.go etc.); stable within a group preserves
|
|
2850
|
+
* FTS order. Returns [] when nothing matches; a qualified lookup that finds no
|
|
2851
|
+
* exact match returns [] rather than a misleading fuzzy file hit (#173); a
|
|
2852
|
+
* bare name with no exact match falls back to the single top fuzzy result.
|
|
2853
|
+
*/
|
|
2854
|
+
findSymbolMatches(cg, symbol) {
|
|
3219
2855
|
const isQualified = /[.\/]|::/.test(symbol);
|
|
3220
|
-
|
|
2856
|
+
// For a bare name, enumerate EVERY exact-name definition via the direct index
|
|
2857
|
+
// (not FTS, which caps + ranks): tokio's `poll` has 50+ defs and the one the
|
|
2858
|
+
// caller wants (`Harness::poll` at harness.rs:153) ranks below any search cut,
|
|
2859
|
+
// so it could be neither rendered nor pinned by the file/line disambiguator —
|
|
2860
|
+
// and the agent Read it. With the full set, the multi-overload render + the
|
|
2861
|
+
// file/line filter can both reach it.
|
|
2862
|
+
if (!isQualified) {
|
|
2863
|
+
const exact = cg.getNodesByName(symbol);
|
|
2864
|
+
if (exact.length > 0) {
|
|
2865
|
+
return [...exact].sort((a, b) => ((0, generated_detection_1.isGeneratedFile)(a.filePath) ? 1 : 0) - ((0, generated_detection_1.isGeneratedFile)(b.filePath) ? 1 : 0));
|
|
2866
|
+
}
|
|
2867
|
+
// No exact match — use the single top fuzzy result (e.g. a file basename).
|
|
2868
|
+
const fuzzy = cg.searchNodes(symbol, { limit: 10 });
|
|
2869
|
+
return fuzzy[0] ? [fuzzy[0].node] : [];
|
|
2870
|
+
}
|
|
2871
|
+
// Qualified lookup (`Session.request`, `stage_apply::run`): FTS + matchesSymbol.
|
|
2872
|
+
const limit = 50;
|
|
3221
2873
|
let results = cg.searchNodes(symbol, { limit });
|
|
3222
|
-
// FTS strips colons
|
|
3223
|
-
//
|
|
3224
|
-
//
|
|
2874
|
+
// FTS strips colons, so `stage_apply::run` searches the literal
|
|
2875
|
+
// `stage_applyrun` and finds nothing. Re-search by the bare last part and
|
|
2876
|
+
// let `matchesSymbol` filter by qualifier.
|
|
3225
2877
|
if (isQualified && results.length === 0) {
|
|
3226
2878
|
const tail = lastQualifierPart(symbol);
|
|
3227
2879
|
if (tail && tail !== symbol)
|
|
3228
2880
|
results = cg.searchNodes(tail, { limit });
|
|
3229
2881
|
}
|
|
3230
|
-
if (results.length === 0
|
|
3231
|
-
return
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
const aGen = (0, generated_detection_1.isGeneratedFile)(a.node.filePath) ? 1 : 0;
|
|
3244
|
-
const bGen = (0, generated_detection_1.isGeneratedFile)(b.node.filePath) ? 1 : 0;
|
|
3245
|
-
return aGen - bGen;
|
|
3246
|
-
});
|
|
3247
|
-
// Multiple exact matches - pick first, note the others
|
|
3248
|
-
const picked = ranked[0].node;
|
|
3249
|
-
const others = ranked.slice(1).map(r => `${r.node.name} (${r.node.kind}) at ${r.node.filePath}:${r.node.startLine}`);
|
|
3250
|
-
const note = `\n\n> **Note:** ${ranked.length} symbols named "${symbol}". Showing results for \`${picked.filePath}:${picked.startLine}\`. Others: ${others.join(', ')}`;
|
|
3251
|
-
return { node: picked, note };
|
|
3252
|
-
}
|
|
3253
|
-
// No exact match. For qualified lookups, don't silently fall back
|
|
3254
|
-
// to a fuzzy result — the user typed a specific qualifier, and
|
|
3255
|
-
// resolving `stage_apply::nonexistent_fn` to the unrelated
|
|
3256
|
-
// `stage_apply.rs` file would be actively misleading (#173).
|
|
3257
|
-
if (isQualified)
|
|
3258
|
-
return null;
|
|
3259
|
-
return { node: results[0].node, note: '' };
|
|
2882
|
+
if (results.length === 0)
|
|
2883
|
+
return [];
|
|
2884
|
+
const exactMatches = results.filter((r) => this.matchesSymbol(r.node, symbol));
|
|
2885
|
+
if (exactMatches.length === 0) {
|
|
2886
|
+
// No exact match — a qualified lookup must not fall back to a fuzzy file
|
|
2887
|
+
// hit (#173); a bare name may use the single top fuzzy result.
|
|
2888
|
+
return isQualified ? [] : results[0] ? [results[0].node] : [];
|
|
2889
|
+
}
|
|
2890
|
+
// Down-rank generated files (.pb.go, .pulsar.go, _grpc.pb.go, …) so a flow
|
|
2891
|
+
// query prefers the keeper implementation over the protobuf-generated stub.
|
|
2892
|
+
return [...exactMatches]
|
|
2893
|
+
.sort((a, b) => ((0, generated_detection_1.isGeneratedFile)(a.node.filePath) ? 1 : 0) - ((0, generated_detection_1.isGeneratedFile)(b.node.filePath) ? 1 : 0))
|
|
2894
|
+
.map((r) => r.node);
|
|
3260
2895
|
}
|
|
3261
2896
|
/**
|
|
3262
2897
|
* Find ALL symbols matching a name. Used by callers/callees/impact to aggregate
|
|
@@ -3398,9 +3033,6 @@ class ToolHandler {
|
|
|
3398
3033
|
}
|
|
3399
3034
|
return lines.join('\n');
|
|
3400
3035
|
}
|
|
3401
|
-
formatTaskContext(context) {
|
|
3402
|
-
return context.summary || 'No context found';
|
|
3403
|
-
}
|
|
3404
3036
|
textResult(text) {
|
|
3405
3037
|
return {
|
|
3406
3038
|
content: [{ type: 'text', text }],
|