@colbymchenry/codegraph-darwin-x64 0.9.5 → 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.
Files changed (163) hide show
  1. package/lib/dist/bin/codegraph.js +10 -25
  2. package/lib/dist/bin/codegraph.js.map +1 -1
  3. package/lib/dist/context/formatter.d.ts.map +1 -1
  4. package/lib/dist/context/formatter.js +25 -6
  5. package/lib/dist/context/formatter.js.map +1 -1
  6. package/lib/dist/context/index.d.ts.map +1 -1
  7. package/lib/dist/context/index.js +31 -0
  8. package/lib/dist/context/index.js.map +1 -1
  9. package/lib/dist/db/queries.d.ts +75 -0
  10. package/lib/dist/db/queries.d.ts.map +1 -1
  11. package/lib/dist/db/queries.js +213 -3
  12. package/lib/dist/db/queries.js.map +1 -1
  13. package/lib/dist/extraction/generated-detection.d.ts +30 -0
  14. package/lib/dist/extraction/generated-detection.d.ts.map +1 -0
  15. package/lib/dist/extraction/generated-detection.js +80 -0
  16. package/lib/dist/extraction/generated-detection.js.map +1 -0
  17. package/lib/dist/extraction/grammars.d.ts +1 -1
  18. package/lib/dist/extraction/grammars.d.ts.map +1 -1
  19. package/lib/dist/extraction/grammars.js +15 -0
  20. package/lib/dist/extraction/grammars.js.map +1 -1
  21. package/lib/dist/extraction/index.js +4 -4
  22. package/lib/dist/extraction/index.js.map +1 -1
  23. package/lib/dist/extraction/languages/c-cpp.d.ts.map +1 -1
  24. package/lib/dist/extraction/languages/c-cpp.js +45 -0
  25. package/lib/dist/extraction/languages/c-cpp.js.map +1 -1
  26. package/lib/dist/extraction/languages/csharp.d.ts.map +1 -1
  27. package/lib/dist/extraction/languages/csharp.js +2 -1
  28. package/lib/dist/extraction/languages/csharp.js.map +1 -1
  29. package/lib/dist/extraction/languages/go.d.ts.map +1 -1
  30. package/lib/dist/extraction/languages/go.js +12 -0
  31. package/lib/dist/extraction/languages/go.js.map +1 -1
  32. package/lib/dist/extraction/languages/java.d.ts.map +1 -1
  33. package/lib/dist/extraction/languages/java.js +6 -0
  34. package/lib/dist/extraction/languages/java.js.map +1 -1
  35. package/lib/dist/extraction/languages/kotlin.d.ts.map +1 -1
  36. package/lib/dist/extraction/languages/kotlin.js +6 -0
  37. package/lib/dist/extraction/languages/kotlin.js.map +1 -1
  38. package/lib/dist/extraction/mybatis-extractor.d.ts +48 -0
  39. package/lib/dist/extraction/mybatis-extractor.d.ts.map +1 -0
  40. package/lib/dist/extraction/mybatis-extractor.js +198 -0
  41. package/lib/dist/extraction/mybatis-extractor.js.map +1 -0
  42. package/lib/dist/extraction/tree-sitter-types.d.ts +10 -0
  43. package/lib/dist/extraction/tree-sitter-types.d.ts.map +1 -1
  44. package/lib/dist/extraction/tree-sitter.d.ts +58 -0
  45. package/lib/dist/extraction/tree-sitter.d.ts.map +1 -1
  46. package/lib/dist/extraction/tree-sitter.js +393 -9
  47. package/lib/dist/extraction/tree-sitter.js.map +1 -1
  48. package/lib/dist/extraction/wasm-runtime-flags.d.ts.map +1 -1
  49. package/lib/dist/extraction/wasm-runtime-flags.js +1 -0
  50. package/lib/dist/extraction/wasm-runtime-flags.js.map +1 -1
  51. package/lib/dist/index.d.ts +32 -1
  52. package/lib/dist/index.d.ts.map +1 -1
  53. package/lib/dist/index.js +49 -1
  54. package/lib/dist/index.js.map +1 -1
  55. package/lib/dist/installer/config-writer.d.ts +7 -8
  56. package/lib/dist/installer/config-writer.d.ts.map +1 -1
  57. package/lib/dist/installer/config-writer.js +7 -27
  58. package/lib/dist/installer/config-writer.js.map +1 -1
  59. package/lib/dist/installer/index.d.ts +3 -20
  60. package/lib/dist/installer/index.d.ts.map +1 -1
  61. package/lib/dist/installer/index.js +8 -39
  62. package/lib/dist/installer/index.js.map +1 -1
  63. package/lib/dist/installer/instructions-template.d.ts +11 -21
  64. package/lib/dist/installer/instructions-template.d.ts.map +1 -1
  65. package/lib/dist/installer/instructions-template.js +12 -56
  66. package/lib/dist/installer/instructions-template.js.map +1 -1
  67. package/lib/dist/installer/targets/antigravity.d.ts +57 -0
  68. package/lib/dist/installer/targets/antigravity.d.ts.map +1 -0
  69. package/lib/dist/installer/targets/antigravity.js +308 -0
  70. package/lib/dist/installer/targets/antigravity.js.map +1 -0
  71. package/lib/dist/installer/targets/claude.d.ts +10 -1
  72. package/lib/dist/installer/targets/claude.d.ts.map +1 -1
  73. package/lib/dist/installer/targets/claude.js +25 -40
  74. package/lib/dist/installer/targets/claude.js.map +1 -1
  75. package/lib/dist/installer/targets/codex.d.ts.map +1 -1
  76. package/lib/dist/installer/targets/codex.js +15 -13
  77. package/lib/dist/installer/targets/codex.js.map +1 -1
  78. package/lib/dist/installer/targets/cursor.d.ts.map +1 -1
  79. package/lib/dist/installer/targets/cursor.js +9 -38
  80. package/lib/dist/installer/targets/cursor.js.map +1 -1
  81. package/lib/dist/installer/targets/gemini.d.ts +26 -0
  82. package/lib/dist/installer/targets/gemini.d.ts.map +1 -0
  83. package/lib/dist/installer/targets/gemini.js +167 -0
  84. package/lib/dist/installer/targets/gemini.js.map +1 -0
  85. package/lib/dist/installer/targets/hermes.d.ts.map +1 -1
  86. package/lib/dist/installer/targets/hermes.js +57 -3
  87. package/lib/dist/installer/targets/hermes.js.map +1 -1
  88. package/lib/dist/installer/targets/kiro.d.ts +27 -0
  89. package/lib/dist/installer/targets/kiro.d.ts.map +1 -0
  90. package/lib/dist/installer/targets/kiro.js +178 -0
  91. package/lib/dist/installer/targets/kiro.js.map +1 -0
  92. package/lib/dist/installer/targets/opencode.d.ts.map +1 -1
  93. package/lib/dist/installer/targets/opencode.js +15 -13
  94. package/lib/dist/installer/targets/opencode.js.map +1 -1
  95. package/lib/dist/installer/targets/registry.d.ts.map +1 -1
  96. package/lib/dist/installer/targets/registry.js +6 -0
  97. package/lib/dist/installer/targets/registry.js.map +1 -1
  98. package/lib/dist/installer/targets/types.d.ts +1 -16
  99. package/lib/dist/installer/targets/types.d.ts.map +1 -1
  100. package/lib/dist/mcp/engine.d.ts +6 -1
  101. package/lib/dist/mcp/engine.d.ts.map +1 -1
  102. package/lib/dist/mcp/engine.js +9 -4
  103. package/lib/dist/mcp/engine.js.map +1 -1
  104. package/lib/dist/mcp/server-instructions.d.ts +1 -1
  105. package/lib/dist/mcp/server-instructions.d.ts.map +1 -1
  106. package/lib/dist/mcp/server-instructions.js +2 -0
  107. package/lib/dist/mcp/server-instructions.js.map +1 -1
  108. package/lib/dist/mcp/tools.d.ts +31 -0
  109. package/lib/dist/mcp/tools.d.ts.map +1 -1
  110. package/lib/dist/mcp/tools.js +556 -55
  111. package/lib/dist/mcp/tools.js.map +1 -1
  112. package/lib/dist/resolution/callback-synthesizer.d.ts +2 -2
  113. package/lib/dist/resolution/callback-synthesizer.d.ts.map +1 -1
  114. package/lib/dist/resolution/callback-synthesizer.js +235 -29
  115. package/lib/dist/resolution/callback-synthesizer.js.map +1 -1
  116. package/lib/dist/resolution/frameworks/java.d.ts.map +1 -1
  117. package/lib/dist/resolution/frameworks/java.js +270 -1
  118. package/lib/dist/resolution/frameworks/java.js.map +1 -1
  119. package/lib/dist/resolution/frameworks/nestjs.d.ts.map +1 -1
  120. package/lib/dist/resolution/frameworks/nestjs.js +324 -0
  121. package/lib/dist/resolution/frameworks/nestjs.js.map +1 -1
  122. package/lib/dist/resolution/go-module.d.ts +26 -0
  123. package/lib/dist/resolution/go-module.d.ts.map +1 -0
  124. package/lib/dist/resolution/go-module.js +78 -0
  125. package/lib/dist/resolution/go-module.js.map +1 -0
  126. package/lib/dist/resolution/import-resolver.d.ts +28 -0
  127. package/lib/dist/resolution/import-resolver.d.ts.map +1 -1
  128. package/lib/dist/resolution/import-resolver.js +571 -4
  129. package/lib/dist/resolution/import-resolver.js.map +1 -1
  130. package/lib/dist/resolution/index.d.ts +10 -0
  131. package/lib/dist/resolution/index.d.ts.map +1 -1
  132. package/lib/dist/resolution/index.js +117 -0
  133. package/lib/dist/resolution/index.js.map +1 -1
  134. package/lib/dist/resolution/name-matcher.d.ts.map +1 -1
  135. package/lib/dist/resolution/name-matcher.js +212 -0
  136. package/lib/dist/resolution/name-matcher.js.map +1 -1
  137. package/lib/dist/resolution/types.d.ts +29 -0
  138. package/lib/dist/resolution/types.d.ts.map +1 -1
  139. package/lib/dist/sync/git-hooks.d.ts.map +1 -1
  140. package/lib/dist/sync/git-hooks.js +2 -0
  141. package/lib/dist/sync/git-hooks.js.map +1 -1
  142. package/lib/dist/sync/index.d.ts +1 -1
  143. package/lib/dist/sync/index.d.ts.map +1 -1
  144. package/lib/dist/sync/index.js +2 -1
  145. package/lib/dist/sync/index.js.map +1 -1
  146. package/lib/dist/sync/watcher.d.ts +10 -0
  147. package/lib/dist/sync/watcher.d.ts.map +1 -1
  148. package/lib/dist/sync/watcher.js +28 -4
  149. package/lib/dist/sync/watcher.js.map +1 -1
  150. package/lib/dist/sync/worktree.d.ts.map +1 -1
  151. package/lib/dist/sync/worktree.js +1 -0
  152. package/lib/dist/sync/worktree.js.map +1 -1
  153. package/lib/dist/types.d.ts +1 -1
  154. package/lib/dist/types.d.ts.map +1 -1
  155. package/lib/dist/types.js +2 -0
  156. package/lib/dist/types.js.map +1 -1
  157. package/lib/node_modules/.package-lock.json +1 -1
  158. package/lib/package.json +1 -1
  159. package/package.json +1 -1
  160. package/lib/dist/installer/claude-md-template.d.ts +0 -14
  161. package/lib/dist/installer/claude-md-template.d.ts.map +0 -1
  162. package/lib/dist/installer/claude-md-template.js +0 -21
  163. package/lib/dist/installer/claude-md-template.js.map +0 -1
@@ -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: true,
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 this FIRST for any "how does X work", architecture, feature, or bug-context question. Composes search + node + callers + callees and returns entry points, related symbols, and key code in ONE call usually enough to answer with no further search/Read/Grep. Prefer this over chaining codegraph_search + codegraph_node, and over codegraph_explore. NOTE: provides CODE context, not product requirements; for new features still clarify UX/edge cases with the user.',
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: 'Find all functions/methods that call a specific symbol. Useful for understanding usage patterns and impact of changes.',
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: 'Find all functions/methods that a specific symbol calls. Useful for understanding dependencies and code flow.',
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: 'Analyze the impact radius of changing a symbol. Shows what code could be affected by modifications.',
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: 'Get ONE symbol\'s details (location, signature, docstring) PLUS its TRAIL — what it calls and what calls it, each with file:line. Pass includeCode=true for source (functions return their body; containers return a member outline). Use this to WALK the call graph hop-by-hop — node a symbol, then node one of its trail entries — the structural, no-Read way to follow "what calls/triggers/handles X" across files. For a broad first overview of many symbols at once use codegraph_explore; use node to drill along a specific path from there. (If a trail is empty on a non-leaf, that hop is likely dynamic dispatch — read just that line.) Source returned with includeCode is the verbatim live file content — identical to Read.',
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: 'Returns source for SEVERAL related symbols grouped by file, plus a relationship map, in ONE capped call. This is the efficient way to inspect many related symbols at once — strongly prefer it over a series of codegraph_node or Read calls (each separate call re-reads the whole context, so 8 node calls cost far more than 1 explore). Use it after codegraph_context when you need to see the actual source of several symbols. Query with specific symbol/file/code terms, NOT natural-language sentences — run codegraph_search first to find names. Bad: "how are agent prompts loaded and passed to the CLI". Good: "renderStaticScene drawElementOnCanvas ShapeCache renderElement.ts". The code it returns is the VERBATIM live file source (byte-for-byte identical to Read), line-numbered — not a summary; treat files it shows as already Read, no need to re-open them.',
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-equivalentdo 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: 'Get the status of the CodeGraph index, including statistics about indexed files, nodes, and edges.',
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: 'REQUIRED for file/folder exploration. Get the project file structure from the CodeGraph index. Returns a tree view of all indexed files with metadata (language, symbol count). Much faster than Glob/filesystem scanning. Use this FIRST when exploring project structure, finding files, or understanding codebase organization.',
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: 'Trace the CALL PATH between two symbols — "how does <from> reach/become <to>?" Returns the chain of functions from one to the other (each hop with file:line and its body inlined, plus the outgoing calls of the destination itself) in ONE call. This is something grep/Read structurally cannot do: there is no text pattern for "the path from A to B". Ideal for flow questions — how an update triggers a render, how a request reaches a handler, how a QuerySet becomes SQL. If no static path exists the chain likely breaks at dynamic dispatch (callbacks/descriptors/metaclasses); the tool says where and points you to codegraph_node to bridge it.',
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 (updaterender, requesthandler, QuerySetSQL). 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
- const visible = allow
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
- const formatted = this.formatSearchResults(results);
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
- const maxNodes = args.maxNodes || 20;
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
- const fromTry = fromMatches.nodes.slice(0, 3);
1134
- const toTry = toMatches.nodes.slice(0, 3);
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
- for (const f of fromTry) {
1138
- for (const t of toTry) {
1139
- const p = cg.findPath(f.id, t.id, edgeKinds);
1140
- if (!p || p.length <= 1)
1141
- continue;
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. Surface the
1154
- // start symbol's outgoing calls so the agent can bridge the gap.
1155
- const start = fromTry[0];
1156
- const callees = cg.getCallees(start.id).slice(0, 10)
1157
- .map(c => `${c.node.name} (${c.node.filePath}:${c.node.startLine})`);
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 (callees.length > 0) {
1170
- lines.push('', `**${start.name} statically calls:** ${callees.join(', ')}`);
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
- return this.textResult(lines.join('\n') + fromMatches.note + toMatches.note);
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
- const relevantFiles = [...fileGroups.entries()].filter(([, group]) => group.score >= 3);
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;
@@ -2149,9 +2622,20 @@ class ToolHandler {
2149
2622
  if (allFiles.length === 0) {
2150
2623
  return this.textResult('No files indexed. Run `codegraph index` first.');
2151
2624
  }
2152
- // Filter by path prefix
2153
- let files = pathFilter
2154
- ? allFiles.filter(f => f.path.startsWith(pathFilter) || f.path.startsWith('./' + pathFilter))
2625
+ // Filter by path prefix. Stored paths are project-relative POSIX (e.g.
2626
+ // "src/foo.ts"), but agents commonly pass project-root variants like "/",
2627
+ // ".", "./", "" or Windows-style "src\foo" — and prefixes with leading
2628
+ // "/", "./" or "\". Normalize all of those before matching so the agent
2629
+ // gets results instead of falling back to Read/Glob (see #426).
2630
+ const normalizedFilter = pathFilter
2631
+ ? pathFilter
2632
+ .replace(/\\/g, '/')
2633
+ .replace(/^(?:\.?\/+)+/, '')
2634
+ .replace(/^\.$/, '')
2635
+ .replace(/\/+$/, '')
2636
+ : '';
2637
+ let files = normalizedFilter
2638
+ ? allFiles.filter(f => f.path === normalizedFilter || f.path.startsWith(normalizedFilter + '/'))
2155
2639
  : allFiles;
2156
2640
  // Filter by glob pattern
2157
2641
  if (pattern) {
@@ -2369,10 +2853,19 @@ class ToolHandler {
2369
2853
  return { node: exactMatches[0].node, note: '' };
2370
2854
  }
2371
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
+ });
2372
2865
  // Multiple exact matches - pick first, note the others
2373
- const picked = exactMatches[0].node;
2374
- const others = exactMatches.slice(1).map(r => `${r.node.name} (${r.node.kind}) at ${r.node.filePath}:${r.node.startLine}`);
2375
- const note = `\n\n> **Note:** ${exactMatches.length} symbols named "${symbol}". Showing results for \`${picked.filePath}:${picked.startLine}\`. Others: ${others.join(', ')}`;
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(', ')}`;
2376
2869
  return { node: picked, note };
2377
2870
  }
2378
2871
  // No exact match. For qualified lookups, don't silently fall back
@@ -2405,9 +2898,17 @@ class ToolHandler {
2405
2898
  const node = exactMatches[0]?.node ?? results[0].node;
2406
2899
  return { nodes: [node], note: '' };
2407
2900
  }
2408
- const locations = exactMatches.map(r => `${r.node.kind} at ${r.node.filePath}:${r.node.startLine}`);
2409
- const note = `\n\n> **Note:** Aggregated results across ${exactMatches.length} symbols named "${symbol}": ${locations.join(', ')}`;
2410
- return { nodes: exactMatches.map(r => r.node), note };
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 };
2411
2912
  }
2412
2913
  /**
2413
2914
  * Truncate output if it exceeds the maximum length