@colbymchenry/codegraph-darwin-x64 0.9.6 → 0.9.7
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 +10 -25
- package/lib/dist/bin/codegraph.js.map +1 -1
- package/lib/dist/context/formatter.d.ts.map +1 -1
- package/lib/dist/context/formatter.js +25 -6
- package/lib/dist/context/formatter.js.map +1 -1
- package/lib/dist/context/index.d.ts.map +1 -1
- package/lib/dist/context/index.js +31 -0
- package/lib/dist/context/index.js.map +1 -1
- package/lib/dist/db/queries.d.ts +74 -0
- package/lib/dist/db/queries.d.ts.map +1 -1
- package/lib/dist/db/queries.js +182 -0
- package/lib/dist/db/queries.js.map +1 -1
- package/lib/dist/extraction/generated-detection.d.ts +30 -0
- package/lib/dist/extraction/generated-detection.d.ts.map +1 -0
- package/lib/dist/extraction/generated-detection.js +80 -0
- package/lib/dist/extraction/generated-detection.js.map +1 -0
- package/lib/dist/extraction/index.js +4 -4
- package/lib/dist/extraction/index.js.map +1 -1
- package/lib/dist/extraction/languages/java.d.ts.map +1 -1
- package/lib/dist/extraction/languages/java.js +6 -0
- package/lib/dist/extraction/languages/java.js.map +1 -1
- package/lib/dist/extraction/languages/kotlin.d.ts.map +1 -1
- package/lib/dist/extraction/languages/kotlin.js +6 -0
- package/lib/dist/extraction/languages/kotlin.js.map +1 -1
- package/lib/dist/extraction/tree-sitter-types.d.ts +10 -0
- package/lib/dist/extraction/tree-sitter-types.d.ts.map +1 -1
- package/lib/dist/extraction/tree-sitter.d.ts +25 -0
- package/lib/dist/extraction/tree-sitter.d.ts.map +1 -1
- package/lib/dist/extraction/tree-sitter.js +124 -0
- package/lib/dist/extraction/tree-sitter.js.map +1 -1
- package/lib/dist/extraction/wasm-runtime-flags.d.ts.map +1 -1
- package/lib/dist/extraction/wasm-runtime-flags.js +1 -0
- package/lib/dist/extraction/wasm-runtime-flags.js.map +1 -1
- package/lib/dist/index.d.ts +31 -0
- package/lib/dist/index.d.ts.map +1 -1
- package/lib/dist/index.js +29 -0
- package/lib/dist/index.js.map +1 -1
- package/lib/dist/installer/config-writer.d.ts +7 -8
- package/lib/dist/installer/config-writer.d.ts.map +1 -1
- package/lib/dist/installer/config-writer.js +7 -27
- package/lib/dist/installer/config-writer.js.map +1 -1
- package/lib/dist/installer/index.d.ts +2 -19
- package/lib/dist/installer/index.d.ts.map +1 -1
- package/lib/dist/installer/index.js +5 -36
- package/lib/dist/installer/index.js.map +1 -1
- package/lib/dist/installer/instructions-template.d.ts +11 -21
- package/lib/dist/installer/instructions-template.d.ts.map +1 -1
- package/lib/dist/installer/instructions-template.js +12 -56
- package/lib/dist/installer/instructions-template.js.map +1 -1
- package/lib/dist/installer/targets/antigravity.d.ts.map +1 -1
- package/lib/dist/installer/targets/antigravity.js +1 -0
- package/lib/dist/installer/targets/antigravity.js.map +1 -1
- package/lib/dist/installer/targets/claude.d.ts +10 -1
- package/lib/dist/installer/targets/claude.d.ts.map +1 -1
- package/lib/dist/installer/targets/claude.js +25 -40
- package/lib/dist/installer/targets/claude.js.map +1 -1
- package/lib/dist/installer/targets/codex.d.ts.map +1 -1
- package/lib/dist/installer/targets/codex.js +15 -13
- package/lib/dist/installer/targets/codex.js.map +1 -1
- package/lib/dist/installer/targets/cursor.d.ts.map +1 -1
- package/lib/dist/installer/targets/cursor.js +9 -38
- package/lib/dist/installer/targets/cursor.js.map +1 -1
- package/lib/dist/installer/targets/gemini.d.ts.map +1 -1
- package/lib/dist/installer/targets/gemini.js +15 -13
- package/lib/dist/installer/targets/gemini.js.map +1 -1
- package/lib/dist/installer/targets/kiro.d.ts.map +1 -1
- package/lib/dist/installer/targets/kiro.js +9 -27
- package/lib/dist/installer/targets/kiro.js.map +1 -1
- package/lib/dist/installer/targets/opencode.d.ts.map +1 -1
- package/lib/dist/installer/targets/opencode.js +15 -13
- package/lib/dist/installer/targets/opencode.js.map +1 -1
- package/lib/dist/installer/targets/types.d.ts +0 -15
- package/lib/dist/installer/targets/types.d.ts.map +1 -1
- package/lib/dist/mcp/engine.d.ts +6 -1
- package/lib/dist/mcp/engine.d.ts.map +1 -1
- package/lib/dist/mcp/engine.js +9 -4
- package/lib/dist/mcp/engine.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 +2 -0
- package/lib/dist/mcp/server-instructions.js.map +1 -1
- package/lib/dist/mcp/tools.d.ts +31 -0
- package/lib/dist/mcp/tools.d.ts.map +1 -1
- package/lib/dist/mcp/tools.js +542 -52
- package/lib/dist/mcp/tools.js.map +1 -1
- package/lib/dist/resolution/callback-synthesizer.d.ts.map +1 -1
- package/lib/dist/resolution/callback-synthesizer.js +156 -27
- package/lib/dist/resolution/callback-synthesizer.js.map +1 -1
- package/lib/dist/resolution/import-resolver.d.ts +10 -0
- package/lib/dist/resolution/import-resolver.d.ts.map +1 -1
- package/lib/dist/resolution/import-resolver.js +34 -0
- package/lib/dist/resolution/import-resolver.js.map +1 -1
- package/lib/dist/resolution/index.d.ts.map +1 -1
- package/lib/dist/resolution/index.js +15 -0
- package/lib/dist/resolution/index.js.map +1 -1
- package/lib/dist/sync/git-hooks.d.ts.map +1 -1
- package/lib/dist/sync/git-hooks.js +2 -0
- package/lib/dist/sync/git-hooks.js.map +1 -1
- package/lib/dist/sync/worktree.d.ts.map +1 -1
- package/lib/dist/sync/worktree.js +1 -0
- package/lib/dist/sync/worktree.js.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/installer/claude-md-template.d.ts +0 -14
- package/lib/dist/installer/claude-md-template.d.ts.map +0 -1
- package/lib/dist/installer/claude-md-template.js +0 -21
- package/lib/dist/installer/claude-md-template.js.map +0 -1
package/lib/dist/mcp/tools.js
CHANGED
|
@@ -48,7 +48,9 @@ const worktree_1 = require("../sync/worktree");
|
|
|
48
48
|
const crypto_1 = require("crypto");
|
|
49
49
|
const fs_1 = require("fs");
|
|
50
50
|
const utils_1 = require("../utils");
|
|
51
|
+
const generated_detection_1 = require("../extraction/generated-detection");
|
|
51
52
|
const os_1 = require("os");
|
|
53
|
+
const pathModule = __importStar(require("path"));
|
|
52
54
|
const path_1 = require("path");
|
|
53
55
|
/** Maximum output length to prevent context bloat (characters) */
|
|
54
56
|
const MAX_OUTPUT_LENGTH = 15000;
|
|
@@ -105,18 +107,40 @@ function getExploreBudget(fileCount) {
|
|
|
105
107
|
return 5;
|
|
106
108
|
}
|
|
107
109
|
function getExploreOutputBudget(fileCount) {
|
|
110
|
+
if (fileCount < 150) {
|
|
111
|
+
return {
|
|
112
|
+
// ITER3: revert iter2's aggressive body shrink (forced Read fallback —
|
|
113
|
+
// the per-file 2.5K cap pushed the agent to Read instead of node).
|
|
114
|
+
// Back to the iter1 shape (13K/4/3.8K) but keep the test-file
|
|
115
|
+
// hard-exclude. The cost lever for this tier lives in handleContext
|
|
116
|
+
// (steering the agent to stop after 1-2 calls), not in this budget.
|
|
117
|
+
maxOutputChars: 13000,
|
|
118
|
+
defaultMaxFiles: 4,
|
|
119
|
+
maxCharsPerFile: 3800,
|
|
120
|
+
gapThreshold: 7,
|
|
121
|
+
maxSymbolsInFileHeader: 5,
|
|
122
|
+
maxEdgesPerRelationshipKind: 4,
|
|
123
|
+
includeRelationships: false,
|
|
124
|
+
includeAdditionalFiles: false,
|
|
125
|
+
includeCompletenessSignal: false,
|
|
126
|
+
includeBudgetNote: false,
|
|
127
|
+
excludeLowValueFiles: true,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
108
130
|
if (fileCount < 500) {
|
|
109
131
|
return {
|
|
132
|
+
// ITER3: same revert/keep-filter pattern as <150.
|
|
110
133
|
maxOutputChars: 18000,
|
|
111
134
|
defaultMaxFiles: 5,
|
|
112
135
|
maxCharsPerFile: 3800,
|
|
113
136
|
gapThreshold: 8,
|
|
114
137
|
maxSymbolsInFileHeader: 6,
|
|
115
138
|
maxEdgesPerRelationshipKind: 6,
|
|
116
|
-
includeRelationships:
|
|
139
|
+
includeRelationships: false,
|
|
117
140
|
includeAdditionalFiles: false,
|
|
118
141
|
includeCompletenessSignal: false,
|
|
119
142
|
includeBudgetNote: false,
|
|
143
|
+
excludeLowValueFiles: true,
|
|
120
144
|
};
|
|
121
145
|
}
|
|
122
146
|
if (fileCount < 5000) {
|
|
@@ -136,6 +160,7 @@ function getExploreOutputBudget(fileCount) {
|
|
|
136
160
|
includeAdditionalFiles: true,
|
|
137
161
|
includeCompletenessSignal: true,
|
|
138
162
|
includeBudgetNote: true,
|
|
163
|
+
excludeLowValueFiles: false,
|
|
139
164
|
};
|
|
140
165
|
}
|
|
141
166
|
if (fileCount < 15000) {
|
|
@@ -150,6 +175,7 @@ function getExploreOutputBudget(fileCount) {
|
|
|
150
175
|
includeAdditionalFiles: true,
|
|
151
176
|
includeCompletenessSignal: true,
|
|
152
177
|
includeBudgetNote: true,
|
|
178
|
+
excludeLowValueFiles: false,
|
|
153
179
|
};
|
|
154
180
|
}
|
|
155
181
|
return {
|
|
@@ -163,6 +189,7 @@ function getExploreOutputBudget(fileCount) {
|
|
|
163
189
|
includeAdditionalFiles: true,
|
|
164
190
|
includeCompletenessSignal: true,
|
|
165
191
|
includeBudgetNote: true,
|
|
192
|
+
excludeLowValueFiles: false,
|
|
166
193
|
};
|
|
167
194
|
}
|
|
168
195
|
/**
|
|
@@ -323,7 +350,7 @@ exports.tools = [
|
|
|
323
350
|
},
|
|
324
351
|
{
|
|
325
352
|
name: 'codegraph_context',
|
|
326
|
-
description: 'PRIMARY TOOL — call
|
|
353
|
+
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.',
|
|
327
354
|
inputSchema: {
|
|
328
355
|
type: 'object',
|
|
329
356
|
properties: {
|
|
@@ -348,7 +375,7 @@ exports.tools = [
|
|
|
348
375
|
},
|
|
349
376
|
{
|
|
350
377
|
name: 'codegraph_callers',
|
|
351
|
-
description: '
|
|
378
|
+
description: 'List functions that call <symbol>. For deep flow use codegraph_trace.',
|
|
352
379
|
inputSchema: {
|
|
353
380
|
type: 'object',
|
|
354
381
|
properties: {
|
|
@@ -368,7 +395,7 @@ exports.tools = [
|
|
|
368
395
|
},
|
|
369
396
|
{
|
|
370
397
|
name: 'codegraph_callees',
|
|
371
|
-
description: '
|
|
398
|
+
description: 'List functions that <symbol> calls. For deep flow use codegraph_trace.',
|
|
372
399
|
inputSchema: {
|
|
373
400
|
type: 'object',
|
|
374
401
|
properties: {
|
|
@@ -388,7 +415,7 @@ exports.tools = [
|
|
|
388
415
|
},
|
|
389
416
|
{
|
|
390
417
|
name: 'codegraph_impact',
|
|
391
|
-
description: '
|
|
418
|
+
description: 'List symbols affected by changing <symbol>. Use before a refactor.',
|
|
392
419
|
inputSchema: {
|
|
393
420
|
type: 'object',
|
|
394
421
|
properties: {
|
|
@@ -408,7 +435,7 @@ exports.tools = [
|
|
|
408
435
|
},
|
|
409
436
|
{
|
|
410
437
|
name: 'codegraph_node',
|
|
411
|
-
description: '
|
|
438
|
+
description: 'One symbol\'s location, signature, callers/callees trail. includeCode=true returns the verbatim body. Use codegraph_trace for full paths instead of chaining nodes.',
|
|
412
439
|
inputSchema: {
|
|
413
440
|
type: 'object',
|
|
414
441
|
properties: {
|
|
@@ -428,7 +455,7 @@ exports.tools = [
|
|
|
428
455
|
},
|
|
429
456
|
{
|
|
430
457
|
name: 'codegraph_explore',
|
|
431
|
-
description: '
|
|
458
|
+
description: 'Source of SEVERAL related symbols grouped by file, in one capped call. Query is a bag of symbol/file names (not a question). Returned source is verbatim Read-equivalent — do not re-open shown files. Prefer over chained codegraph_node.',
|
|
432
459
|
inputSchema: {
|
|
433
460
|
type: 'object',
|
|
434
461
|
properties: {
|
|
@@ -448,7 +475,7 @@ exports.tools = [
|
|
|
448
475
|
},
|
|
449
476
|
{
|
|
450
477
|
name: 'codegraph_status',
|
|
451
|
-
description: '
|
|
478
|
+
description: 'Index health check (files / nodes / edges). Skip unless debugging.',
|
|
452
479
|
inputSchema: {
|
|
453
480
|
type: 'object',
|
|
454
481
|
properties: {
|
|
@@ -458,7 +485,7 @@ exports.tools = [
|
|
|
458
485
|
},
|
|
459
486
|
{
|
|
460
487
|
name: 'codegraph_files',
|
|
461
|
-
description: '
|
|
488
|
+
description: 'Indexed file tree with language + symbol counts. Faster than Glob for project layout.',
|
|
462
489
|
inputSchema: {
|
|
463
490
|
type: 'object',
|
|
464
491
|
properties: {
|
|
@@ -491,7 +518,7 @@ exports.tools = [
|
|
|
491
518
|
},
|
|
492
519
|
{
|
|
493
520
|
name: 'codegraph_trace',
|
|
494
|
-
description: '
|
|
521
|
+
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.',
|
|
495
522
|
inputSchema: {
|
|
496
523
|
type: 'object',
|
|
497
524
|
properties: {
|
|
@@ -528,6 +555,14 @@ class ToolHandler {
|
|
|
528
555
|
// once and every later tool call reuses the result — never shelling out to
|
|
529
556
|
// git on the hot path. `undefined` = not computed yet; `null` = no mismatch.
|
|
530
557
|
worktreeMismatchCache = new Map();
|
|
558
|
+
// Gate that the MCP engine pokes after `cg.open()` so the first tool call
|
|
559
|
+
// blocks on the post-open filesystem reconcile (catch-up sync). Without
|
|
560
|
+
// this, a tool call that races past `catchUpSync()` serves rows for files
|
|
561
|
+
// that were deleted (or edited) while no MCP server was running — and the
|
|
562
|
+
// per-file staleness banner can't help, because `getPendingFiles()` is
|
|
563
|
+
// populated by the watcher, not by catch-up. Cleared on first await so
|
|
564
|
+
// subsequent calls don't pay any cost.
|
|
565
|
+
catchUpGate = null;
|
|
531
566
|
constructor(cg) {
|
|
532
567
|
this.cg = cg;
|
|
533
568
|
}
|
|
@@ -537,6 +572,16 @@ class ToolHandler {
|
|
|
537
572
|
setDefaultCodeGraph(cg) {
|
|
538
573
|
this.cg = cg;
|
|
539
574
|
}
|
|
575
|
+
/**
|
|
576
|
+
* Engine-only: register the catch-up sync promise so the next `execute()`
|
|
577
|
+
* call awaits it before serving. The handler swallows rejections (the
|
|
578
|
+
* engine logs them) so a sync failure never propagates as a tool error;
|
|
579
|
+
* we still want to serve a best-effort result over the same potentially-
|
|
580
|
+
* stale data, which is what would have happened without the gate.
|
|
581
|
+
*/
|
|
582
|
+
setCatchUpGate(p) {
|
|
583
|
+
this.catchUpGate = p;
|
|
584
|
+
}
|
|
540
585
|
/**
|
|
541
586
|
* Record the directory the server tried to resolve the default project from.
|
|
542
587
|
* Used only to make the "no default project" error actionable.
|
|
@@ -579,7 +624,7 @@ class ToolHandler {
|
|
|
579
624
|
*/
|
|
580
625
|
getTools() {
|
|
581
626
|
const allow = this.toolAllowlist();
|
|
582
|
-
|
|
627
|
+
let visible = allow
|
|
583
628
|
? exports.tools.filter(t => allow.has(t.name.replace(/^codegraph_/, '')))
|
|
584
629
|
: exports.tools;
|
|
585
630
|
if (!this.cg)
|
|
@@ -587,6 +632,39 @@ class ToolHandler {
|
|
|
587
632
|
try {
|
|
588
633
|
const stats = this.cg.getStats();
|
|
589
634
|
const budget = getExploreBudget(stats.fileCount);
|
|
635
|
+
// Tiny-repo tool gating: on projects under TINY_REPO_FILE_THRESHOLD
|
|
636
|
+
// files, only expose the 5 core tools (search, context, node,
|
|
637
|
+
// explore, trace). The 5 omitted tools (callers, callees, impact,
|
|
638
|
+
// status, files) reduce to one grep at this scale.
|
|
639
|
+
//
|
|
640
|
+
// n=2 audits ruled out cutting below 5 tools:
|
|
641
|
+
// - 3-tool gate (search + context + trace): cost regressed on
|
|
642
|
+
// cobra/ky/sinatra. The agent fell back to raw Reads to cover
|
|
643
|
+
// what codegraph_node + codegraph_explore would have answered.
|
|
644
|
+
// - 1-tool gate (search only): catastrophic regression — express
|
|
645
|
+
// went from -43% WIN to +107% LOSS. With only search, the agent
|
|
646
|
+
// can't navigate the call graph structurally and reads everything.
|
|
647
|
+
//
|
|
648
|
+
// 5 is the empirical lower bound. Tools beyond search/context/
|
|
649
|
+
// node/explore/trace pay overhead that the agent doesn't recoup
|
|
650
|
+
// on tiny-repo flow questions.
|
|
651
|
+
// ITER4: raise threshold 150 → 500 so single-file frameworks
|
|
652
|
+
// (sinatra at 159, slim_framework around 200) also get the
|
|
653
|
+
// 5-tool surface. The empirical 5-tool floor was set on <150
|
|
654
|
+
// probes; iter3 measurement showed sinatra is structurally the
|
|
655
|
+
// SAME problem as cobra (single-file WITHOUT-arm Read wins),
|
|
656
|
+
// so it deserves the same gating.
|
|
657
|
+
const TINY_REPO_FILE_THRESHOLD = 500;
|
|
658
|
+
const TINY_REPO_CORE_TOOLS = new Set([
|
|
659
|
+
'codegraph_search',
|
|
660
|
+
'codegraph_context',
|
|
661
|
+
'codegraph_node',
|
|
662
|
+
'codegraph_explore',
|
|
663
|
+
'codegraph_trace',
|
|
664
|
+
]);
|
|
665
|
+
if (stats.fileCount < TINY_REPO_FILE_THRESHOLD) {
|
|
666
|
+
visible = visible.filter(t => TINY_REPO_CORE_TOOLS.has(t.name));
|
|
667
|
+
}
|
|
590
668
|
return visible.map(tool => {
|
|
591
669
|
if (tool.name === 'codegraph_explore') {
|
|
592
670
|
return {
|
|
@@ -842,6 +920,19 @@ class ToolHandler {
|
|
|
842
920
|
*/
|
|
843
921
|
async execute(toolName, args) {
|
|
844
922
|
try {
|
|
923
|
+
// Block the first tool call on the engine's post-open reconcile so we
|
|
924
|
+
// never serve rows for files deleted/edited while no MCP server was
|
|
925
|
+
// running. The gate is cleared after first await — subsequent calls
|
|
926
|
+
// pay nothing. Catch-up failures are logged by the engine; we
|
|
927
|
+
// proceed regardless so a transient sync error never breaks tools.
|
|
928
|
+
if (this.catchUpGate) {
|
|
929
|
+
const gate = this.catchUpGate;
|
|
930
|
+
this.catchUpGate = null;
|
|
931
|
+
try {
|
|
932
|
+
await gate;
|
|
933
|
+
}
|
|
934
|
+
catch { /* engine already logged */ }
|
|
935
|
+
}
|
|
845
936
|
// Honor the optional tool allowlist (CODEGRAPH_MCP_TOOLS): a trimmed
|
|
846
937
|
// surface rejects ablated tools defensively even if a client cached them.
|
|
847
938
|
if (!this.isToolAllowed(toolName)) {
|
|
@@ -935,7 +1026,15 @@ class ToolHandler {
|
|
|
935
1026
|
if (results.length === 0) {
|
|
936
1027
|
return this.textResult(`No results found for "${query}"`);
|
|
937
1028
|
}
|
|
938
|
-
|
|
1029
|
+
// Down-rank generated files within the FTS-returned set so a search
|
|
1030
|
+
// for "Send" surfaces the hand-written keeper before .pb.go stubs
|
|
1031
|
+
// that share the name. Stable: only reorders generated vs. not.
|
|
1032
|
+
const ranked = [...results].sort((a, b) => {
|
|
1033
|
+
const aGen = (0, generated_detection_1.isGeneratedFile)(a.node.filePath) ? 1 : 0;
|
|
1034
|
+
const bGen = (0, generated_detection_1.isGeneratedFile)(b.node.filePath) ? 1 : 0;
|
|
1035
|
+
return aGen - bGen;
|
|
1036
|
+
});
|
|
1037
|
+
const formatted = this.formatSearchResults(ranked);
|
|
939
1038
|
return this.textResult(this.truncateOutput(formatted));
|
|
940
1039
|
}
|
|
941
1040
|
/**
|
|
@@ -951,7 +1050,27 @@ class ToolHandler {
|
|
|
951
1050
|
markSessionConsulted(sessionId);
|
|
952
1051
|
}
|
|
953
1052
|
const cg = this.getCodeGraph(args.projectPath);
|
|
954
|
-
|
|
1053
|
+
// On tiny repos (<150 files), trim maxNodes hard — the entire repo
|
|
1054
|
+
// is grep-able in a turn so a 20-node context is wasted budget.
|
|
1055
|
+
// 8 covers the typical 1-3 entry-point + their immediate neighbors
|
|
1056
|
+
// without dragging in the rest of the small codebase.
|
|
1057
|
+
let defaultMaxNodes = 20;
|
|
1058
|
+
let isTinyRepo = false;
|
|
1059
|
+
let isSmallRepo = false;
|
|
1060
|
+
try {
|
|
1061
|
+
const stats = cg.getStats();
|
|
1062
|
+
if (stats.fileCount < 150) {
|
|
1063
|
+
defaultMaxNodes = 8;
|
|
1064
|
+
isTinyRepo = true;
|
|
1065
|
+
}
|
|
1066
|
+
else if (stats.fileCount < 500) {
|
|
1067
|
+
isSmallRepo = true;
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
catch {
|
|
1071
|
+
// stats failure — fall back to the standard default
|
|
1072
|
+
}
|
|
1073
|
+
const maxNodes = args.maxNodes || defaultMaxNodes;
|
|
955
1074
|
const includeCode = args.includeCode !== false;
|
|
956
1075
|
const context = await cg.buildContext(task, {
|
|
957
1076
|
maxNodes,
|
|
@@ -963,12 +1082,189 @@ class ToolHandler {
|
|
|
963
1082
|
const reminder = isFeatureQuery
|
|
964
1083
|
? '\n\n⚠️ **Ask user:** UX preferences, edge cases, acceptance criteria'
|
|
965
1084
|
: '';
|
|
1085
|
+
// Auto-trace for flow queries: when the task is asking "how does X
|
|
1086
|
+
// reach/flow/propagate from A to B", run the trace internally and
|
|
1087
|
+
// append its body to the context response. Saves the agent the
|
|
1088
|
+
// follow-up codegraph_trace call that was the #2 cost driver on
|
|
1089
|
+
// multi-module flow questions (Q3 / etcd Q2 in the audit).
|
|
1090
|
+
const flowTrace = await this.maybeInlineFlowTrace(task, cg);
|
|
1091
|
+
// Iter3 — sufficiency steering on small repos.
|
|
1092
|
+
//
|
|
1093
|
+
// Measured economics on tiny (<150) and small (<500) projects: every
|
|
1094
|
+
// additional MCP tool call costs ~$0.02-0.05 in cache-write tokens
|
|
1095
|
+
// (5K-15K per response at $3.75/1M). The agent reflexively follows
|
|
1096
|
+
// codegraph_context with explore/node even when the context response
|
|
1097
|
+
// is already sufficient — that pattern drove the cost gap that
|
|
1098
|
+
// smaller bodies (iter2) failed to close (smaller bodies just shifted
|
|
1099
|
+
// the agent to Read instead). Direct directive on small-repo
|
|
1100
|
+
// responses: tell the agent the context call IS the comprehensive
|
|
1101
|
+
// pass for a project of this size and that follow-ups should be
|
|
1102
|
+
// narrow (trace from→to, node single-symbol) — not another broad
|
|
1103
|
+
// explore that re-bundles the same content.
|
|
1104
|
+
// ITER4: unified strong directive for both tiny (<150) and small
|
|
1105
|
+
// (<500) tiers — measured iter3 result was that the soft <500
|
|
1106
|
+
// wording was IGNORED on sinatra (5 tool calls, +92% loss) while
|
|
1107
|
+
// the strong <150 wording was followed on cobra/slim (3 calls,
|
|
1108
|
+
// -21%/-22% wins). The single-file-framework problem (sinatra)
|
|
1109
|
+
// is structurally the same as cobra's; both deserve the same
|
|
1110
|
+
// sufficiency steering.
|
|
1111
|
+
let smallRepoTail = '';
|
|
1112
|
+
let smallRepoRouteInline = '';
|
|
1113
|
+
if (isTinyRepo || isSmallRepo) {
|
|
1114
|
+
// Iter12: backend-computed routing manifest for routing queries.
|
|
1115
|
+
// Builds a URL → handler map directly from the graph (each route
|
|
1116
|
+
// node has a `references` edge to its handler), then inlines the
|
|
1117
|
+
// top handler file's source. The agent gets the canonical
|
|
1118
|
+
// routing answer in one MCP call — no need to parse framework
|
|
1119
|
+
// DSL or grep for handlers.
|
|
1120
|
+
//
|
|
1121
|
+
// Replaces iter10's raw route-file inline. The manifest is more
|
|
1122
|
+
// information-dense (parsed URL→handler map vs raw config DSL)
|
|
1123
|
+
// and we still inline the top handler file's source so the agent
|
|
1124
|
+
// has the implementation bodies inline too.
|
|
1125
|
+
const isRouteQuery = /\b(route|routes|routing|request|handler|endpoint|api|controller|middleware|dispatch|invok)/i.test(task);
|
|
1126
|
+
if (isRouteQuery) {
|
|
1127
|
+
try {
|
|
1128
|
+
const manifest = cg.getRoutingManifest(40);
|
|
1129
|
+
if (manifest) {
|
|
1130
|
+
// 1) Compact URL→handler list (~30-60 lines, ~1-2KB).
|
|
1131
|
+
const lines = [
|
|
1132
|
+
`\n\n## Routing manifest (${manifest.totalRoutes} routes, top handler file holds ${manifest.topHandlerFileCount})`,
|
|
1133
|
+
'',
|
|
1134
|
+
'| URL | Handler | Location |',
|
|
1135
|
+
'|---|---|---|',
|
|
1136
|
+
];
|
|
1137
|
+
for (const e of manifest.entries) {
|
|
1138
|
+
lines.push(`| \`${e.url}\` | \`${e.handler}\` | ${e.handlerFile}:${e.handlerLine} |`);
|
|
1139
|
+
}
|
|
1140
|
+
// 2) Inline the top handler file's source.
|
|
1141
|
+
if (manifest.topHandlerFile && manifest.topHandlerFileCount >= 2) {
|
|
1142
|
+
try {
|
|
1143
|
+
const fullPath = pathModule.join(cg.getProjectRoot(), manifest.topHandlerFile);
|
|
1144
|
+
const stat = (0, fs_1.statSync)(fullPath);
|
|
1145
|
+
if (stat.size > 0 && stat.size <= 16000) {
|
|
1146
|
+
const source = (0, fs_1.readFileSync)(fullPath, 'utf-8');
|
|
1147
|
+
const capped = source.length > 7000 ? source.slice(0, 7000) + '\n... (truncated)' : source;
|
|
1148
|
+
const ext = (manifest.topHandlerFile.match(/\.([a-z]+)$/i)?.[1] || '').toLowerCase();
|
|
1149
|
+
const lang = ext === 'rb' ? 'ruby' : ext === 'py' ? 'python' :
|
|
1150
|
+
ext === 'go' ? 'go' : ext === 'rs' ? 'rust' :
|
|
1151
|
+
ext === 'js' || ext === 'jsx' ? 'javascript' :
|
|
1152
|
+
ext === 'ts' || ext === 'tsx' ? 'typescript' :
|
|
1153
|
+
ext === 'java' ? 'java' : ext === 'kt' ? 'kotlin' :
|
|
1154
|
+
ext === 'cs' ? 'csharp' : ext === 'php' ? 'php' :
|
|
1155
|
+
ext === 'swift' ? 'swift' : ext === 'yml' || ext === 'yaml' ? 'yaml' : '';
|
|
1156
|
+
lines.push('');
|
|
1157
|
+
lines.push(`### Top handler file (\`${manifest.topHandlerFile}\` — ${manifest.topHandlerFileCount}/${manifest.totalRoutes} routes, full source inlined — do NOT Read)`);
|
|
1158
|
+
lines.push('');
|
|
1159
|
+
lines.push('```' + lang);
|
|
1160
|
+
lines.push(capped);
|
|
1161
|
+
lines.push('```');
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
catch { /* file read failed, skip the source inline */ }
|
|
1165
|
+
}
|
|
1166
|
+
smallRepoRouteInline = lines.join('\n');
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
catch {
|
|
1170
|
+
// Manifest build failed — drop silently
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
const sizeQualifier = isTinyRepo ? 'under 150' : 'under 500';
|
|
1174
|
+
const routingClause = smallRepoRouteInline
|
|
1175
|
+
? ' The URL→handler manifest and top handler file are also inlined above — answer routing questions from them.'
|
|
1176
|
+
: '';
|
|
1177
|
+
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.`;
|
|
1178
|
+
}
|
|
966
1179
|
// buildContext returns string when format is 'markdown'
|
|
967
1180
|
if (typeof context === 'string') {
|
|
968
|
-
return this.textResult(this.truncateOutput(context + reminder));
|
|
1181
|
+
return this.textResult(this.truncateOutput(context + flowTrace + reminder + smallRepoRouteInline + smallRepoTail));
|
|
969
1182
|
}
|
|
970
1183
|
// If it returns TaskContext, format it
|
|
971
|
-
return this.textResult(this.truncateOutput(this.formatTaskContext(context) + reminder));
|
|
1184
|
+
return this.textResult(this.truncateOutput(this.formatTaskContext(context) + flowTrace + reminder + smallRepoRouteInline + smallRepoTail));
|
|
1185
|
+
}
|
|
1186
|
+
/**
|
|
1187
|
+
* Detect a flow-style task ("how does X reach Y", "trace the path from A to B")
|
|
1188
|
+
* and pre-run trace between the most likely endpoints, returning the trace
|
|
1189
|
+
* body to splice into the context response. Returns '' for non-flow queries
|
|
1190
|
+
* or when no plausible endpoint pair can be extracted.
|
|
1191
|
+
*
|
|
1192
|
+
* Conservative by design: only fires when the task has both a clear flow
|
|
1193
|
+
* keyword AND at least two distinct PascalCase / camelCase identifiers.
|
|
1194
|
+
* False positives waste a graph query; false negatives just fall back to
|
|
1195
|
+
* the agent calling trace itself (existing path-proximity wiring handles
|
|
1196
|
+
* disambiguation either way).
|
|
1197
|
+
*/
|
|
1198
|
+
async maybeInlineFlowTrace(task, cg) {
|
|
1199
|
+
const lower = task.toLowerCase();
|
|
1200
|
+
const FLOW_KEYWORDS = [
|
|
1201
|
+
'trace ',
|
|
1202
|
+
'from ',
|
|
1203
|
+
'reach ',
|
|
1204
|
+
'flow ',
|
|
1205
|
+
'propagat',
|
|
1206
|
+
'how does ',
|
|
1207
|
+
'how do ',
|
|
1208
|
+
];
|
|
1209
|
+
if (!FLOW_KEYWORDS.some((k) => lower.includes(k)))
|
|
1210
|
+
return '';
|
|
1211
|
+
// Extract candidate symbols — PascalCase or camelCase identifiers ≥3 chars.
|
|
1212
|
+
// Filter out common non-symbol words and the flow keywords themselves.
|
|
1213
|
+
const STOP_WORDS = new Set([
|
|
1214
|
+
'how', 'does', 'the', 'and', 'from', 'through', 'reach', 'reaches',
|
|
1215
|
+
'flow', 'path', 'trace', 'cross', 'module', 'modules', 'where',
|
|
1216
|
+
'update', 'updates', 'updated', 'when', 'what', 'this', 'that',
|
|
1217
|
+
]);
|
|
1218
|
+
const ids = [];
|
|
1219
|
+
const seen = new Set();
|
|
1220
|
+
const re = /\b([A-Z][a-z]+(?:[A-Z][a-z]*)+|[a-z]+[A-Z][a-z]*(?:[A-Z][a-z]*)*)\b/g;
|
|
1221
|
+
let m;
|
|
1222
|
+
while ((m = re.exec(task)) !== null) {
|
|
1223
|
+
const sym = m[1];
|
|
1224
|
+
if (sym.length < 3)
|
|
1225
|
+
continue;
|
|
1226
|
+
const key = sym.toLowerCase();
|
|
1227
|
+
if (STOP_WORDS.has(key) || seen.has(key))
|
|
1228
|
+
continue;
|
|
1229
|
+
seen.add(key);
|
|
1230
|
+
ids.push(sym);
|
|
1231
|
+
}
|
|
1232
|
+
if (ids.length < 2)
|
|
1233
|
+
return '';
|
|
1234
|
+
// The first two distinct symbols, in order of appearance, are the most
|
|
1235
|
+
// likely from/to endpoints — "from X ... through to Y" naturally places
|
|
1236
|
+
// them in that order in the prose. If the trace fails to connect, it
|
|
1237
|
+
// still returns the inlined endpoint bodies (the trace-failure rewrite).
|
|
1238
|
+
const fromSym = ids[0];
|
|
1239
|
+
const toSym = ids[1];
|
|
1240
|
+
let traceResult;
|
|
1241
|
+
try {
|
|
1242
|
+
traceResult = await this.handleTrace({
|
|
1243
|
+
from: fromSym,
|
|
1244
|
+
to: toSym,
|
|
1245
|
+
projectPath: cg.getProjectRoot(),
|
|
1246
|
+
});
|
|
1247
|
+
}
|
|
1248
|
+
catch {
|
|
1249
|
+
return '';
|
|
1250
|
+
}
|
|
1251
|
+
// Extract the textual body. Defensive: handleTrace's contract is the
|
|
1252
|
+
// standard tool-result shape used elsewhere in this file.
|
|
1253
|
+
const body = traceResult.content
|
|
1254
|
+
?.map((c) => (c.type === 'text' ? c.text : ''))
|
|
1255
|
+
.filter(Boolean)
|
|
1256
|
+
.join('\n')
|
|
1257
|
+
.trim();
|
|
1258
|
+
if (!body)
|
|
1259
|
+
return '';
|
|
1260
|
+
return [
|
|
1261
|
+
'',
|
|
1262
|
+
'## Inline flow trace',
|
|
1263
|
+
'',
|
|
1264
|
+
`Auto-traced \`${fromSym}\` → \`${toSym}\` because the query looks like a flow question. No follow-up codegraph_trace is needed for this pair.`,
|
|
1265
|
+
'',
|
|
1266
|
+
body,
|
|
1267
|
+
].join('\n');
|
|
972
1268
|
}
|
|
973
1269
|
/**
|
|
974
1270
|
* Heuristic to detect if a query looks like a feature request
|
|
@@ -1130,46 +1426,180 @@ class ToolHandler {
|
|
|
1130
1426
|
// (which, on real code, means the flow breaks at dynamic dispatch).
|
|
1131
1427
|
const edgeKinds = ['calls'];
|
|
1132
1428
|
const MAX_HOPS = 7;
|
|
1133
|
-
|
|
1134
|
-
|
|
1429
|
+
// Path-proximity pairing: in a multi-module repo a symbol name like
|
|
1430
|
+
// `EndBlocker` exists in 20+ modules. FTS picks one almost arbitrarily;
|
|
1431
|
+
// the WRONG pair (e.g. simapp's wrapper EndBlocker paired with gov's Tally)
|
|
1432
|
+
// has no static path, falls through to the dynamic-dispatch failure branch,
|
|
1433
|
+
// and surfaces unrelated bodies — exactly the cosmos-Q3 trace failure mode.
|
|
1434
|
+
// Score every from×to combo by shared file-path prefix length; try the
|
|
1435
|
+
// most-co-located pair first (e.g. `x/gov/abci.go::EndBlocker` ×
|
|
1436
|
+
// `x/gov/keeper/tally.go::Tally` share `x/gov/`).
|
|
1437
|
+
//
|
|
1438
|
+
// Consider the FULL candidate set, not just the FTS top-5: the right
|
|
1439
|
+
// EndBlocker for a gov-module flow may rank 8th in FTS but share the
|
|
1440
|
+
// entire `x/gov/` prefix with the destination. Path-proximity supersedes
|
|
1441
|
+
// FTS for this disambiguation. Findpath trials are still capped by
|
|
1442
|
+
// FINDPATH_PAIR_BUDGET below to bound graph traversal cost.
|
|
1443
|
+
const sharedDirPrefixLen = (a, b) => {
|
|
1444
|
+
const aDir = a.replace(/[^/]+$/, '');
|
|
1445
|
+
const bDir = b.replace(/[^/]+$/, '');
|
|
1446
|
+
let i = 0;
|
|
1447
|
+
while (i < aDir.length && i < bDir.length && aDir[i] === bDir[i])
|
|
1448
|
+
i++;
|
|
1449
|
+
return i;
|
|
1450
|
+
};
|
|
1451
|
+
// Cosmos-Q3 surfaced a second-order failure: `enterprise/group/x/group/`
|
|
1452
|
+
// SHARES MORE of its path with `enterprise/group/x/group/keeper/tally.go`
|
|
1453
|
+
// (24 chars) than `x/gov/abci.go` shares with `x/gov/keeper/tally.go`
|
|
1454
|
+
// (6 chars), so pure shared-prefix prefers the side-experiment module
|
|
1455
|
+
// over the canonical one — even though the user's question is clearly
|
|
1456
|
+
// about the main gov module. Penalize candidates living under prefixes
|
|
1457
|
+
// that conventionally hold extensions / experiments / vendored code, so
|
|
1458
|
+
// the canonical-path pair wins even when its shared prefix is short.
|
|
1459
|
+
const isLessCanonicalPath = (p) => /^(enterprise|contrib|examples?|sample|playground|vendor|third[_-]?party|deprecated|legacy)\//i.test(p);
|
|
1460
|
+
const LESS_CANONICAL_PENALTY = 100; // any canonical candidate beats any less-canonical one
|
|
1461
|
+
const scorePair = (a, b) => sharedDirPrefixLen(a, b)
|
|
1462
|
+
- (isLessCanonicalPath(a) ? LESS_CANONICAL_PENALTY : 0)
|
|
1463
|
+
- (isLessCanonicalPath(b) ? LESS_CANONICAL_PENALTY : 0);
|
|
1464
|
+
const fromCands = fromMatches.nodes;
|
|
1465
|
+
const toCands = toMatches.nodes;
|
|
1466
|
+
const pairs = [];
|
|
1467
|
+
for (const f of fromCands) {
|
|
1468
|
+
for (const t of toCands) {
|
|
1469
|
+
pairs.push({ f, t, score: scorePair(f.filePath, t.filePath) });
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
// Sort by shared prefix desc, then by FTS order (already encoded in the
|
|
1473
|
+
// pairs' insertion order — both for f and t). The tiebreaker preserves
|
|
1474
|
+
// findAllSymbols' generated-file-last ranking.
|
|
1475
|
+
pairs.sort((a, b) => b.score - a.score);
|
|
1476
|
+
// Cap how many graph-path probes we attempt so a 50×50 cross-product
|
|
1477
|
+
// doesn't blow up on a god-named symbol like `Get` (well-named flows have
|
|
1478
|
+
// their good pair near the top of the sort anyway).
|
|
1479
|
+
const FINDPATH_PAIR_BUDGET = 20;
|
|
1480
|
+
const fromTry = fromCands;
|
|
1481
|
+
const toTry = toCands;
|
|
1135
1482
|
let path = null;
|
|
1136
1483
|
let overCap = null;
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1484
|
+
let bestPair = null;
|
|
1485
|
+
let triedPairs = 0;
|
|
1486
|
+
for (const { f, t } of pairs) {
|
|
1487
|
+
if (path)
|
|
1488
|
+
break;
|
|
1489
|
+
if (triedPairs >= FINDPATH_PAIR_BUDGET)
|
|
1490
|
+
break;
|
|
1491
|
+
triedPairs++;
|
|
1492
|
+
const p = cg.findPath(f.id, t.id, edgeKinds);
|
|
1493
|
+
if (p && p.length > 1) {
|
|
1142
1494
|
if (p.length <= MAX_HOPS) {
|
|
1143
1495
|
path = p;
|
|
1496
|
+
bestPair = { f, t };
|
|
1144
1497
|
break;
|
|
1145
1498
|
}
|
|
1146
|
-
if (!overCap || p.length < overCap.length)
|
|
1499
|
+
if (!overCap || p.length < overCap.length) {
|
|
1147
1500
|
overCap = p;
|
|
1501
|
+
bestPair = { f, t };
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
else if (!bestPair) {
|
|
1505
|
+
// No path yet — remember the top-scored pair so the failure branch
|
|
1506
|
+
// surfaces the most-co-located candidates' bodies, not whatever FTS
|
|
1507
|
+
// happened to put first.
|
|
1508
|
+
bestPair = { f, t };
|
|
1148
1509
|
}
|
|
1149
|
-
if (path)
|
|
1150
|
-
break;
|
|
1151
1510
|
}
|
|
1152
1511
|
if (!path) {
|
|
1153
|
-
// No static path — almost always a dynamic-dispatch break.
|
|
1154
|
-
//
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1512
|
+
// No static path — almost always a dynamic-dispatch break. INSTEAD of
|
|
1513
|
+
// telling the agent to chase the gap with codegraph_node/callers/callees
|
|
1514
|
+
// (which fans out into 3-4 follow-up tool calls + a Read), inline the
|
|
1515
|
+
// material those would have returned right here. Measured on cosmos-Q3:
|
|
1516
|
+
// the failed-trace + subsequent fan-out used to cost ~2× a single
|
|
1517
|
+
// sufficient trace call; this branch closes that gap.
|
|
1518
|
+
// Prefer the path-proximity-best pair we identified above (e.g. gov's
|
|
1519
|
+
// EndBlocker × gov's Tally) over the FTS top-pick (simapp's wrapper).
|
|
1520
|
+
const start = bestPair?.f ?? fromTry[0];
|
|
1521
|
+
const end = bestPair?.t ?? toTry[0];
|
|
1522
|
+
const fileCache = new Map();
|
|
1158
1523
|
const lines = [
|
|
1159
|
-
`No direct call path from "${from}" to "${to}".`,
|
|
1524
|
+
`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.`,
|
|
1160
1525
|
'',
|
|
1161
|
-
(overCap
|
|
1162
|
-
? `(Only a ${overCap.length}-hop indirect chain connects them — almost certainly a BFS wander through unrelated code, not the real flow.) `
|
|
1163
|
-
: '') +
|
|
1164
|
-
'The direct chain most likely breaks at **dynamic dispatch** (a callback, descriptor, ' +
|
|
1165
|
-
'metaclass, or attribute-as-callable) that static parsing cannot resolve into an edge. ' +
|
|
1166
|
-
`Inspect \`${start.name}\` (${start.filePath}:${start.startLine}) with codegraph_node ` +
|
|
1167
|
-
'(includeCode=true) — its body usually shows the dynamic call to follow next.',
|
|
1168
1526
|
];
|
|
1169
|
-
if (
|
|
1170
|
-
lines.push(
|
|
1527
|
+
if (overCap) {
|
|
1528
|
+
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).`, '');
|
|
1171
1529
|
}
|
|
1172
|
-
|
|
1530
|
+
// Track which node IDs we've already inlined a body for so we don't
|
|
1531
|
+
// double-emit when a callee of FROM is also surfaced separately.
|
|
1532
|
+
const inlinedBodies = new Set();
|
|
1533
|
+
const inlineBody = (n, lineCap, charCap) => {
|
|
1534
|
+
if (inlinedBodies.has(n.id))
|
|
1535
|
+
return false;
|
|
1536
|
+
inlinedBodies.add(n.id);
|
|
1537
|
+
const body = this.sourceRangeAt(cg, n.filePath, n.startLine, n.endLine, fileCache, lineCap, charCap);
|
|
1538
|
+
if (body) {
|
|
1539
|
+
lines.push(body);
|
|
1540
|
+
return true;
|
|
1541
|
+
}
|
|
1542
|
+
return false;
|
|
1543
|
+
};
|
|
1544
|
+
const inlineEndpoint = (label, node) => {
|
|
1545
|
+
lines.push(`### ${label}: \`${node.name}\` (${node.filePath}:${node.startLine}-${node.endLine})`);
|
|
1546
|
+
inlineBody(node, 120, 3600);
|
|
1547
|
+
const callers = cg.getCallers(node.id).slice(0, 6);
|
|
1548
|
+
if (callers.length > 0) {
|
|
1549
|
+
lines.push(`**Callers of \`${node.name}\`:** ` +
|
|
1550
|
+
callers.map(c => `${c.node.name} (${c.node.filePath}:${c.node.startLine})`).join(', '));
|
|
1551
|
+
}
|
|
1552
|
+
const callees = cg.getCallees(node.id).slice(0, 8);
|
|
1553
|
+
if (callees.length > 0) {
|
|
1554
|
+
lines.push(`**\`${node.name}\` calls:** ` +
|
|
1555
|
+
callees.map(c => `${c.node.name} (${c.node.filePath}:${c.node.startLine})`).join(', '));
|
|
1556
|
+
}
|
|
1557
|
+
lines.push('');
|
|
1558
|
+
};
|
|
1559
|
+
inlineEndpoint('FROM', start);
|
|
1560
|
+
if (end.id !== start.id)
|
|
1561
|
+
inlineEndpoint('TO', end);
|
|
1562
|
+
// Inline the OTHER top-level functions/methods in TO's file — that's
|
|
1563
|
+
// where the missing dynamic-dispatch flow usually lives. Concrete
|
|
1564
|
+
// measurement from cosmos-Q1: `msgServer.Send` statically calls only
|
|
1565
|
+
// utility functions (`StringToBytes`, `Wrapf`); its real next-hop
|
|
1566
|
+
// `SendCoins` is invoked via an embedded-interface call (`k.Keeper.SendCoins`)
|
|
1567
|
+
// that static parsing CAN'T see. The flow IS in the same file as the
|
|
1568
|
+
// destination (`x/bank/keeper/send.go`: SendCoins → subUnlockedCoins →
|
|
1569
|
+
// addCoins → setBalance). Pre-inlining those file-mates is what
|
|
1570
|
+
// replaces the agent's "trace fail → search SendCoins → node SendCoins
|
|
1571
|
+
// → trace again" fan-out.
|
|
1572
|
+
const NEIGHBOR_LINES = 40;
|
|
1573
|
+
const NEIGHBOR_CHARS = 1200;
|
|
1574
|
+
const NEIGHBOR_K = 5;
|
|
1575
|
+
const fileSiblings = (anchor) => {
|
|
1576
|
+
// Functions and methods in the same file as the anchor, excluding
|
|
1577
|
+
// the anchor itself and anything we've already inlined. Sort by
|
|
1578
|
+
// distance from the anchor's startLine so the closest symbols come
|
|
1579
|
+
// first (the flow is usually adjacent in the file).
|
|
1580
|
+
const sameFile = cg
|
|
1581
|
+
.getNodesByKind('function')
|
|
1582
|
+
.filter((n) => n.filePath === anchor.filePath)
|
|
1583
|
+
.concat(cg.getNodesByKind('method').filter((n) => n.filePath === anchor.filePath));
|
|
1584
|
+
return sameFile
|
|
1585
|
+
.filter((n) => n.id !== anchor.id && !inlinedBodies.has(n.id))
|
|
1586
|
+
.sort((a, b) => Math.abs(a.startLine - anchor.startLine) - Math.abs(b.startLine - anchor.startLine))
|
|
1587
|
+
.slice(0, NEIGHBOR_K);
|
|
1588
|
+
};
|
|
1589
|
+
const renderSiblings = (label, siblings) => {
|
|
1590
|
+
if (siblings.length === 0)
|
|
1591
|
+
return;
|
|
1592
|
+
lines.push(`### ${label}`);
|
|
1593
|
+
for (const sib of siblings) {
|
|
1594
|
+
lines.push('');
|
|
1595
|
+
lines.push(`- \`${sib.name}\` (${sib.filePath}:${sib.startLine}-${sib.endLine})`);
|
|
1596
|
+
inlineBody(sib, NEIGHBOR_LINES, NEIGHBOR_CHARS);
|
|
1597
|
+
}
|
|
1598
|
+
lines.push('');
|
|
1599
|
+
};
|
|
1600
|
+
renderSiblings(`Other functions in \`${end.filePath}\` (the flow that the dynamic-dispatch hop reaches — bodies inlined)`, fileSiblings(end));
|
|
1601
|
+
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.');
|
|
1602
|
+
return this.textResult(this.truncateOutput(lines.join('\n') + fromMatches.note + toMatches.note));
|
|
1173
1603
|
}
|
|
1174
1604
|
const lines = [
|
|
1175
1605
|
`## Trace: ${from} → ${to}`,
|
|
@@ -1591,9 +2021,46 @@ class ToolHandler {
|
|
|
1591
2021
|
fileGroups.set(node.filePath, group);
|
|
1592
2022
|
}
|
|
1593
2023
|
// Only include files that have entry points or nodes directly connected to entry points
|
|
1594
|
-
|
|
2024
|
+
let relevantFiles = [...fileGroups.entries()].filter(([, group]) => group.score >= 3);
|
|
1595
2025
|
// Extract query terms for relevance checking
|
|
1596
2026
|
const queryTerms = query.toLowerCase().split(/\s+/).filter(t => t.length >= 3);
|
|
2027
|
+
// Test/spec/icon/i18n file detector — used both for the pre-sort hard
|
|
2028
|
+
// filter (tiny tier) and the comparator deprioritization (all tiers).
|
|
2029
|
+
const isLowValue = (p) => {
|
|
2030
|
+
const lp = p.toLowerCase();
|
|
2031
|
+
return (/\/(tests?|__tests?__|spec)\//.test(lp) ||
|
|
2032
|
+
/_test\.go$/.test(lp) ||
|
|
2033
|
+
/(?:^|\/)test_[^/]+\.py$/.test(lp) ||
|
|
2034
|
+
/_test\.py$/.test(lp) ||
|
|
2035
|
+
/_spec\.rb$/.test(lp) ||
|
|
2036
|
+
/_test\.rb$/.test(lp) ||
|
|
2037
|
+
/\.(test|spec)\.[jt]sx?$/.test(lp) ||
|
|
2038
|
+
/(test|spec|tests)\.(java|kt|scala)$/.test(lp) ||
|
|
2039
|
+
/(tests?|spec)\.cs$/.test(lp) ||
|
|
2040
|
+
/tests?\.swift$/.test(lp) ||
|
|
2041
|
+
/_test\.dart$/.test(lp) ||
|
|
2042
|
+
/\bicons?\b/.test(lp) ||
|
|
2043
|
+
/\bi18n\b/.test(lp));
|
|
2044
|
+
};
|
|
2045
|
+
// Tiny-tier hard-exclude: on small projects (`excludeLowValueFiles`
|
|
2046
|
+
// budget flag), one slipped test/spec file dominates the per-file budget
|
|
2047
|
+
// (cobra's `command_test.go` displaced `args.go` and contributed ~10KB of
|
|
2048
|
+
// pure noise to "How does cobra parse commands?"). The sort-step
|
|
2049
|
+
// deprioritization isn't enough at small N. Skip the hard-exclude when
|
|
2050
|
+
// the query itself is about tests — that's the legitimate "explore the
|
|
2051
|
+
// tests" case where the agent does want them.
|
|
2052
|
+
if (budget.excludeLowValueFiles) {
|
|
2053
|
+
const queryMentionsTests = /\b(test|tests|testing|spec|verify|verifies)\b/i.test(query);
|
|
2054
|
+
if (!queryMentionsTests) {
|
|
2055
|
+
const nonLow = relevantFiles.filter(([p]) => !isLowValue(p));
|
|
2056
|
+
// Only apply the hard-filter if we still have at least 2 non-test
|
|
2057
|
+
// candidates after the cut — otherwise the agent is asking about an
|
|
2058
|
+
// area where tests are the only signal, and we should not strip them.
|
|
2059
|
+
if (nonLow.length >= 2) {
|
|
2060
|
+
relevantFiles = nonLow;
|
|
2061
|
+
}
|
|
2062
|
+
}
|
|
2063
|
+
}
|
|
1597
2064
|
// Sort files: highest relevance first, deprioritize low-value files
|
|
1598
2065
|
const sortedFiles = relevantFiles.sort((a, b) => {
|
|
1599
2066
|
const aPath = a[0].toLowerCase();
|
|
@@ -1609,14 +2076,20 @@ class ToolHandler {
|
|
|
1609
2076
|
const bRelevant = hasQueryRelevance(bPath, b[1].nodes);
|
|
1610
2077
|
if (aRelevant !== bRelevant)
|
|
1611
2078
|
return aRelevant ? -1 : 1;
|
|
1612
|
-
// Deprioritize test files, icon files, and i18n files
|
|
1613
|
-
const isLowValue = (p) => /\/(tests?|__tests?__|spec)\//i.test(p) ||
|
|
1614
|
-
/\bicons?\b/i.test(p) ||
|
|
1615
|
-
/\bi18n\b/i.test(p);
|
|
1616
2079
|
const aLow = isLowValue(aPath);
|
|
1617
2080
|
const bLow = isLowValue(bPath);
|
|
1618
2081
|
if (aLow !== bLow)
|
|
1619
2082
|
return aLow ? 1 : -1;
|
|
2083
|
+
// Deprioritize generated source (.pb.go / .pulsar.go / _mocks.go / …) —
|
|
2084
|
+
// the agent rarely needs to see the protobuf scaffold or gomock output
|
|
2085
|
+
// when asking about the actual flow, and dumping their bodies inflates
|
|
2086
|
+
// the response (the cosmos Q3 explore otherwise leads with
|
|
2087
|
+
// `expected_keepers_mocks.go`, displacing the real `tally.go` content
|
|
2088
|
+
// and forcing the agent to Read tally.go anyway).
|
|
2089
|
+
const aGen = (0, generated_detection_1.isGeneratedFile)(a[0]);
|
|
2090
|
+
const bGen = (0, generated_detection_1.isGeneratedFile)(b[0]);
|
|
2091
|
+
if (aGen !== bGen)
|
|
2092
|
+
return aGen ? 1 : -1;
|
|
1620
2093
|
if (a[1].score !== b[1].score)
|
|
1621
2094
|
return b[1].score - a[1].score;
|
|
1622
2095
|
return b[1].nodes.length - a[1].nodes.length;
|
|
@@ -2380,10 +2853,19 @@ class ToolHandler {
|
|
|
2380
2853
|
return { node: exactMatches[0].node, note: '' };
|
|
2381
2854
|
}
|
|
2382
2855
|
if (exactMatches.length > 1) {
|
|
2856
|
+
// Down-rank generated files (.pb.go, .pulsar.go, _grpc.pb.go, …)
|
|
2857
|
+
// so a query like "Send" prefers the keeper implementation over
|
|
2858
|
+
// the protobuf-generated interface stub. Stable sort preserves
|
|
2859
|
+
// FTS order within each group. See generated-detection.ts.
|
|
2860
|
+
const ranked = [...exactMatches].sort((a, b) => {
|
|
2861
|
+
const aGen = (0, generated_detection_1.isGeneratedFile)(a.node.filePath) ? 1 : 0;
|
|
2862
|
+
const bGen = (0, generated_detection_1.isGeneratedFile)(b.node.filePath) ? 1 : 0;
|
|
2863
|
+
return aGen - bGen;
|
|
2864
|
+
});
|
|
2383
2865
|
// Multiple exact matches - pick first, note the others
|
|
2384
|
-
const picked =
|
|
2385
|
-
const others =
|
|
2386
|
-
const note = `\n\n> **Note:** ${
|
|
2866
|
+
const picked = ranked[0].node;
|
|
2867
|
+
const others = ranked.slice(1).map(r => `${r.node.name} (${r.node.kind}) at ${r.node.filePath}:${r.node.startLine}`);
|
|
2868
|
+
const note = `\n\n> **Note:** ${ranked.length} symbols named "${symbol}". Showing results for \`${picked.filePath}:${picked.startLine}\`. Others: ${others.join(', ')}`;
|
|
2387
2869
|
return { node: picked, note };
|
|
2388
2870
|
}
|
|
2389
2871
|
// No exact match. For qualified lookups, don't silently fall back
|
|
@@ -2416,9 +2898,17 @@ class ToolHandler {
|
|
|
2416
2898
|
const node = exactMatches[0]?.node ?? results[0].node;
|
|
2417
2899
|
return { nodes: [node], note: '' };
|
|
2418
2900
|
}
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2901
|
+
// Same generated-file down-rank as findSymbol — keeps callers/callees
|
|
2902
|
+
// /impact aggregation aligned (a query against "Send" returns the
|
|
2903
|
+
// hand-written implementations before the protobuf scaffold).
|
|
2904
|
+
const ranked = [...exactMatches].sort((a, b) => {
|
|
2905
|
+
const aGen = (0, generated_detection_1.isGeneratedFile)(a.node.filePath) ? 1 : 0;
|
|
2906
|
+
const bGen = (0, generated_detection_1.isGeneratedFile)(b.node.filePath) ? 1 : 0;
|
|
2907
|
+
return aGen - bGen;
|
|
2908
|
+
});
|
|
2909
|
+
const locations = ranked.map(r => `${r.node.kind} at ${r.node.filePath}:${r.node.startLine}`);
|
|
2910
|
+
const note = `\n\n> **Note:** Aggregated results across ${ranked.length} symbols named "${symbol}": ${locations.join(', ')}`;
|
|
2911
|
+
return { nodes: ranked.map(r => r.node), note };
|
|
2422
2912
|
}
|
|
2423
2913
|
/**
|
|
2424
2914
|
* Truncate output if it exceeds the maximum length
|