@optave/codegraph 3.9.6 → 3.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (186) hide show
  1. package/README.md +26 -12
  2. package/dist/ast-analysis/engine.d.ts.map +1 -1
  3. package/dist/ast-analysis/engine.js +1 -1
  4. package/dist/ast-analysis/engine.js.map +1 -1
  5. package/dist/ast-analysis/rules/index.d.ts.map +1 -1
  6. package/dist/ast-analysis/rules/index.js +77 -0
  7. package/dist/ast-analysis/rules/index.js.map +1 -1
  8. package/dist/ast-analysis/visitors/ast-store-visitor.d.ts.map +1 -1
  9. package/dist/ast-analysis/visitors/ast-store-visitor.js +50 -8
  10. package/dist/ast-analysis/visitors/ast-store-visitor.js.map +1 -1
  11. package/dist/cli/commands/audit.js +1 -1
  12. package/dist/cli/commands/audit.js.map +1 -1
  13. package/dist/cli/commands/build.d.ts.map +1 -1
  14. package/dist/cli/commands/build.js +2 -0
  15. package/dist/cli/commands/build.js.map +1 -1
  16. package/dist/cli/commands/check.js +1 -1
  17. package/dist/cli/commands/check.js.map +1 -1
  18. package/dist/cli/commands/children.js +1 -1
  19. package/dist/cli/commands/children.js.map +1 -1
  20. package/dist/cli/commands/diff-impact.js +1 -1
  21. package/dist/cli/commands/diff-impact.js.map +1 -1
  22. package/dist/cli/commands/roles.js +1 -1
  23. package/dist/cli/commands/roles.js.map +1 -1
  24. package/dist/cli/commands/structure.js +1 -1
  25. package/dist/cli/commands/structure.js.map +1 -1
  26. package/dist/cli/shared/options.js +1 -1
  27. package/dist/cli/shared/options.js.map +1 -1
  28. package/dist/db/connection.d.ts.map +1 -1
  29. package/dist/db/connection.js +8 -0
  30. package/dist/db/connection.js.map +1 -1
  31. package/dist/domain/graph/builder/context.d.ts +10 -0
  32. package/dist/domain/graph/builder/context.d.ts.map +1 -1
  33. package/dist/domain/graph/builder/context.js +10 -0
  34. package/dist/domain/graph/builder/context.js.map +1 -1
  35. package/dist/domain/graph/builder/helpers.d.ts +7 -2
  36. package/dist/domain/graph/builder/helpers.d.ts.map +1 -1
  37. package/dist/domain/graph/builder/helpers.js +7 -2
  38. package/dist/domain/graph/builder/helpers.js.map +1 -1
  39. package/dist/domain/graph/builder/incremental.d.ts +0 -6
  40. package/dist/domain/graph/builder/incremental.d.ts.map +1 -1
  41. package/dist/domain/graph/builder/incremental.js +6 -23
  42. package/dist/domain/graph/builder/incremental.js.map +1 -1
  43. package/dist/domain/graph/builder/pipeline.d.ts +44 -0
  44. package/dist/domain/graph/builder/pipeline.d.ts.map +1 -1
  45. package/dist/domain/graph/builder/pipeline.js +348 -42
  46. package/dist/domain/graph/builder/pipeline.js.map +1 -1
  47. package/dist/domain/graph/builder/stages/build-edges.d.ts.map +1 -1
  48. package/dist/domain/graph/builder/stages/build-edges.js +8 -2
  49. package/dist/domain/graph/builder/stages/build-edges.js.map +1 -1
  50. package/dist/domain/graph/builder/stages/collect-files.d.ts.map +1 -1
  51. package/dist/domain/graph/builder/stages/collect-files.js +8 -0
  52. package/dist/domain/graph/builder/stages/collect-files.js.map +1 -1
  53. package/dist/domain/graph/builder/stages/detect-changes.d.ts +24 -0
  54. package/dist/domain/graph/builder/stages/detect-changes.d.ts.map +1 -1
  55. package/dist/domain/graph/builder/stages/detect-changes.js +117 -3
  56. package/dist/domain/graph/builder/stages/detect-changes.js.map +1 -1
  57. package/dist/domain/graph/builder/stages/finalize.d.ts.map +1 -1
  58. package/dist/domain/graph/builder/stages/finalize.js +9 -6
  59. package/dist/domain/graph/builder/stages/finalize.js.map +1 -1
  60. package/dist/domain/graph/builder/stages/insert-nodes.d.ts +30 -0
  61. package/dist/domain/graph/builder/stages/insert-nodes.d.ts.map +1 -1
  62. package/dist/domain/graph/builder/stages/insert-nodes.js +36 -13
  63. package/dist/domain/graph/builder/stages/insert-nodes.js.map +1 -1
  64. package/dist/domain/graph/builder/stages/resolve-imports.d.ts.map +1 -1
  65. package/dist/domain/graph/builder/stages/resolve-imports.js +73 -22
  66. package/dist/domain/graph/builder/stages/resolve-imports.js.map +1 -1
  67. package/dist/domain/graph/watcher.d.ts.map +1 -1
  68. package/dist/domain/graph/watcher.js +23 -18
  69. package/dist/domain/graph/watcher.js.map +1 -1
  70. package/dist/domain/parser.d.ts +14 -1
  71. package/dist/domain/parser.d.ts.map +1 -1
  72. package/dist/domain/parser.js +104 -11
  73. package/dist/domain/parser.js.map +1 -1
  74. package/dist/domain/search/models.d.ts +16 -0
  75. package/dist/domain/search/models.d.ts.map +1 -1
  76. package/dist/domain/search/models.js +36 -2
  77. package/dist/domain/search/models.js.map +1 -1
  78. package/dist/domain/wasm-worker-entry.js +20 -13
  79. package/dist/domain/wasm-worker-entry.js.map +1 -1
  80. package/dist/extractors/c.js +25 -6
  81. package/dist/extractors/c.js.map +1 -1
  82. package/dist/extractors/cpp.js +47 -6
  83. package/dist/extractors/cpp.js.map +1 -1
  84. package/dist/extractors/cuda.js +90 -14
  85. package/dist/extractors/cuda.js.map +1 -1
  86. package/dist/extractors/elixir.js +83 -3
  87. package/dist/extractors/elixir.js.map +1 -1
  88. package/dist/extractors/erlang.js +56 -20
  89. package/dist/extractors/erlang.js.map +1 -1
  90. package/dist/extractors/fsharp.d.ts +7 -0
  91. package/dist/extractors/fsharp.d.ts.map +1 -1
  92. package/dist/extractors/fsharp.js +94 -0
  93. package/dist/extractors/fsharp.js.map +1 -1
  94. package/dist/extractors/gleam.js +6 -2
  95. package/dist/extractors/gleam.js.map +1 -1
  96. package/dist/extractors/groovy.js +41 -1
  97. package/dist/extractors/groovy.js.map +1 -1
  98. package/dist/extractors/haskell.js +48 -4
  99. package/dist/extractors/haskell.js.map +1 -1
  100. package/dist/extractors/julia.js +172 -41
  101. package/dist/extractors/julia.js.map +1 -1
  102. package/dist/extractors/kotlin.js +4 -0
  103. package/dist/extractors/kotlin.js.map +1 -1
  104. package/dist/extractors/objc.js +184 -47
  105. package/dist/extractors/objc.js.map +1 -1
  106. package/dist/extractors/python.js +7 -4
  107. package/dist/extractors/python.js.map +1 -1
  108. package/dist/extractors/r.js +93 -52
  109. package/dist/extractors/r.js.map +1 -1
  110. package/dist/extractors/scala.d.ts.map +1 -1
  111. package/dist/extractors/scala.js +18 -32
  112. package/dist/extractors/scala.js.map +1 -1
  113. package/dist/extractors/solidity.js +18 -9
  114. package/dist/extractors/solidity.js.map +1 -1
  115. package/dist/extractors/verilog.js +80 -15
  116. package/dist/extractors/verilog.js.map +1 -1
  117. package/dist/infrastructure/config.d.ts +1 -0
  118. package/dist/infrastructure/config.d.ts.map +1 -1
  119. package/dist/infrastructure/config.js +1 -0
  120. package/dist/infrastructure/config.js.map +1 -1
  121. package/dist/mcp/server.d.ts.map +1 -1
  122. package/dist/mcp/server.js +14 -8
  123. package/dist/mcp/server.js.map +1 -1
  124. package/dist/mcp/tool-registry.d.ts +1 -1
  125. package/dist/mcp/tool-registry.d.ts.map +1 -1
  126. package/dist/mcp/tool-registry.js +23 -5
  127. package/dist/mcp/tool-registry.js.map +1 -1
  128. package/dist/mcp/tools/semantic-search.d.ts +1 -0
  129. package/dist/mcp/tools/semantic-search.d.ts.map +1 -1
  130. package/dist/mcp/tools/semantic-search.js +1 -0
  131. package/dist/mcp/tools/semantic-search.js.map +1 -1
  132. package/dist/types.d.ts +16 -1
  133. package/dist/types.d.ts.map +1 -1
  134. package/grammars/tree-sitter-erlang.wasm +0 -0
  135. package/grammars/tree-sitter-fsharp.wasm +0 -0
  136. package/grammars/tree-sitter-fsharp_signature.wasm +0 -0
  137. package/grammars/tree-sitter-gleam.wasm +0 -0
  138. package/package.json +11 -10
  139. package/src/ast-analysis/engine.ts +3 -1
  140. package/src/ast-analysis/rules/index.ts +87 -0
  141. package/src/ast-analysis/visitors/ast-store-visitor.ts +45 -9
  142. package/src/cli/commands/audit.ts +1 -1
  143. package/src/cli/commands/build.ts +2 -0
  144. package/src/cli/commands/check.ts +1 -1
  145. package/src/cli/commands/children.ts +1 -1
  146. package/src/cli/commands/diff-impact.ts +1 -1
  147. package/src/cli/commands/roles.ts +1 -1
  148. package/src/cli/commands/structure.ts +1 -1
  149. package/src/cli/shared/options.ts +1 -1
  150. package/src/db/connection.ts +8 -0
  151. package/src/domain/graph/builder/context.ts +10 -0
  152. package/src/domain/graph/builder/helpers.ts +8 -3
  153. package/src/domain/graph/builder/incremental.ts +6 -41
  154. package/src/domain/graph/builder/pipeline.ts +404 -41
  155. package/src/domain/graph/builder/stages/build-edges.ts +9 -2
  156. package/src/domain/graph/builder/stages/collect-files.ts +9 -0
  157. package/src/domain/graph/builder/stages/detect-changes.ts +130 -4
  158. package/src/domain/graph/builder/stages/finalize.ts +9 -6
  159. package/src/domain/graph/builder/stages/insert-nodes.ts +38 -14
  160. package/src/domain/graph/builder/stages/resolve-imports.ts +79 -25
  161. package/src/domain/graph/watcher.ts +21 -23
  162. package/src/domain/parser.ts +110 -10
  163. package/src/domain/search/models.ts +37 -2
  164. package/src/domain/wasm-worker-entry.ts +20 -13
  165. package/src/extractors/c.ts +27 -8
  166. package/src/extractors/cpp.ts +50 -8
  167. package/src/extractors/cuda.ts +90 -16
  168. package/src/extractors/elixir.ts +75 -3
  169. package/src/extractors/erlang.ts +63 -20
  170. package/src/extractors/fsharp.ts +104 -0
  171. package/src/extractors/gleam.ts +7 -2
  172. package/src/extractors/groovy.ts +45 -1
  173. package/src/extractors/haskell.ts +45 -4
  174. package/src/extractors/julia.ts +164 -43
  175. package/src/extractors/kotlin.ts +4 -0
  176. package/src/extractors/objc.ts +171 -47
  177. package/src/extractors/python.ts +5 -3
  178. package/src/extractors/r.ts +88 -48
  179. package/src/extractors/scala.ts +24 -36
  180. package/src/extractors/solidity.ts +17 -8
  181. package/src/extractors/verilog.ts +83 -15
  182. package/src/infrastructure/config.ts +1 -0
  183. package/src/mcp/server.ts +16 -9
  184. package/src/mcp/tool-registry.ts +28 -5
  185. package/src/mcp/tools/semantic-search.ts +2 -0
  186. package/src/types.ts +16 -0
@@ -122,7 +122,7 @@ function handlePyFunctionDef(node: TreeSitterNode, ctx: ExtractorOutput): void {
122
122
  const parentClass = findPythonParentClass(node);
123
123
  const fullName = parentClass ? `${parentClass}.${nameNode.text}` : nameNode.text;
124
124
  const kind = parentClass ? 'method' : 'function';
125
- const fnChildren = extractPythonParameters(node);
125
+ const fnChildren = extractPythonParameters(node, parentClass !== null);
126
126
  ctx.definitions.push({
127
127
  name: fullName,
128
128
  kind,
@@ -238,7 +238,7 @@ function handlePyImportFrom(node: TreeSitterNode, ctx: ExtractorOutput): void {
238
238
 
239
239
  // ── Python-specific helpers ─────────────────────────────────────────────────
240
240
 
241
- function extractPythonParameters(fnNode: TreeSitterNode): SubDeclaration[] {
241
+ function extractPythonParameters(fnNode: TreeSitterNode, isMethod: boolean): SubDeclaration[] {
242
242
  const params: SubDeclaration[] = [];
243
243
  const paramsNode = fnNode.childForFieldName('parameters') || findChild(fnNode, 'parameters');
244
244
  if (!paramsNode) return params;
@@ -246,7 +246,9 @@ function extractPythonParameters(fnNode: TreeSitterNode): SubDeclaration[] {
246
246
  const child = paramsNode.child(i);
247
247
  if (!child) continue;
248
248
  const param = extractSinglePyParam(child);
249
- if (param) params.push(param);
249
+ if (!param) continue;
250
+ if (isMethod && (param.name === 'self' || param.name === 'cls')) continue;
251
+ params.push(param);
250
252
  }
251
253
  return params;
252
254
  }
@@ -125,11 +125,16 @@ function handleCall(node: TreeSitterNode, ctx: ExtractorOutput): void {
125
125
  return;
126
126
  }
127
127
 
128
- if (funcName === 'setGeneric' || funcName === 'setMethod') {
128
+ if (funcName === 'setGeneric') {
129
129
  handleSetGeneric(node, ctx);
130
130
  return;
131
131
  }
132
132
 
133
+ if (funcName === 'setMethod') {
134
+ handleSetMethod(node, ctx);
135
+ return;
136
+ }
137
+
133
138
  // Regular call
134
139
  if (funcNode.type === 'identifier') {
135
140
  ctx.calls.push({ name: funcName, line: node.startPosition.row + 1 });
@@ -147,7 +152,10 @@ function handleCall(node: TreeSitterNode, ctx: ExtractorOutput): void {
147
152
  }
148
153
 
149
154
  function handleLibraryCall(node: TreeSitterNode, ctx: ExtractorOutput): void {
150
- // Find the package name in arguments
155
+ // Find the package name in arguments. For named arguments like
156
+ // `library(package = dplyr)`, prefer the field-named `value` child of the
157
+ // `argument` node so we extract `dplyr` (the value), not `package` (the
158
+ // parameter name). Keeps native (Rust) and WASM extractors in parity.
151
159
  for (let i = 0; i < node.childCount; i++) {
152
160
  const child = node.child(i);
153
161
  if (!child) continue;
@@ -174,9 +182,27 @@ function handleLibraryCall(node: TreeSitterNode, ctx: ExtractorOutput): void {
174
182
  }
175
183
  // Argument might be wrapped
176
184
  if (arg.type === 'argument') {
177
- const id = findChild(arg, 'identifier') || findChild(arg, 'string');
178
- if (id) {
179
- const text = id.text.replace(/^["']|["']$/g, '');
185
+ // Prefer the `value` field (correct for named arguments).
186
+ const valueNode = arg.childForFieldName('value');
187
+ let pick: TreeSitterNode | null = null;
188
+ if (valueNode && (valueNode.type === 'string' || valueNode.type === 'identifier')) {
189
+ pick = valueNode;
190
+ } else {
191
+ // Fallback: skip the parameter-name child if the grammar exposes
192
+ // it via the `name` field, then pick the first string/identifier.
193
+ const nameNode = arg.childForFieldName('name');
194
+ for (let k = 0; k < arg.childCount; k++) {
195
+ const inner = arg.child(k);
196
+ if (!inner) continue;
197
+ if (nameNode && inner.id === nameNode.id) continue;
198
+ if (inner.type === 'string' || inner.type === 'identifier') {
199
+ pick = inner;
200
+ break;
201
+ }
202
+ }
203
+ }
204
+ if (pick) {
205
+ const text = pick.text.replace(/^["']|["']$/g, '');
180
206
  ctx.imports.push({
181
207
  source: text,
182
208
  names: [text],
@@ -191,47 +217,55 @@ function handleLibraryCall(node: TreeSitterNode, ctx: ExtractorOutput): void {
191
217
  }
192
218
 
193
219
  function handleSourceCall(node: TreeSitterNode, ctx: ExtractorOutput): void {
194
- for (let i = 0; i < node.childCount; i++) {
195
- const child = node.child(i);
196
- if (!child || child.type !== 'arguments') continue;
197
- for (let j = 0; j < child.childCount; j++) {
198
- const arg = child.child(j);
199
- if (!arg) continue;
200
- if (arg.type === 'string') {
201
- const text = arg.text.replace(/^["']|["']$/g, '');
202
- ctx.imports.push({
203
- source: text,
204
- names: ['source'],
205
- line: node.startPosition.row + 1,
206
- });
207
- return;
208
- }
209
- }
210
- }
220
+ // source() only accepts string literals `source(varname)` is not an import.
221
+ const path = firstStringArgument(node);
222
+ if (path === null) return;
223
+ ctx.imports.push({
224
+ source: path,
225
+ names: ['source'],
226
+ line: node.startPosition.row + 1,
227
+ });
211
228
  }
212
229
 
213
230
  function handleSetClass(node: TreeSitterNode, ctx: ExtractorOutput): void {
214
- for (let i = 0; i < node.childCount; i++) {
215
- const child = node.child(i);
216
- if (!child || child.type !== 'arguments') continue;
217
- for (let j = 0; j < child.childCount; j++) {
218
- const arg = child.child(j);
219
- if (!arg) continue;
220
- if (arg.type === 'string') {
221
- const name = arg.text.replace(/^["']|["']$/g, '');
222
- ctx.definitions.push({
223
- name,
224
- kind: 'class',
225
- line: node.startPosition.row + 1,
226
- endLine: nodeEndLine(node),
227
- });
228
- return;
229
- }
230
- }
231
- }
231
+ const name = firstStringArgument(node);
232
+ if (name === null) return;
233
+ ctx.definitions.push({
234
+ name,
235
+ kind: 'class',
236
+ line: node.startPosition.row + 1,
237
+ endLine: nodeEndLine(node),
238
+ });
232
239
  }
233
240
 
234
241
  function handleSetGeneric(node: TreeSitterNode, ctx: ExtractorOutput): void {
242
+ const name = firstStringArgument(node);
243
+ if (name === null) return;
244
+ ctx.definitions.push({
245
+ name,
246
+ kind: 'function',
247
+ line: node.startPosition.row + 1,
248
+ endLine: nodeEndLine(node),
249
+ });
250
+ }
251
+
252
+ // setMethod("greet", "Person", function(x) ...) registers an implementation of
253
+ // the generic `greet` — it is not a new top-level definition. Emitting a
254
+ // definition here produced two `function` nodes with the same name (one from
255
+ // setGeneric, one from setMethod) and broke resolution. Emit a call edge to
256
+ // the generic instead; the method body's calls are still picked up by the
257
+ // recursive walk of the anonymous function argument.
258
+ function handleSetMethod(node: TreeSitterNode, ctx: ExtractorOutput): void {
259
+ const name = firstStringArgument(node);
260
+ if (name === null) return;
261
+ ctx.calls.push({ name, line: node.startPosition.row + 1 });
262
+ }
263
+
264
+ // tree-sitter-r wraps each positional argument in an `argument` node that
265
+ // contains the actual `string` (or `identifier`) child, so the inner string
266
+ // must be unwrapped — checking `child.type === 'string'` directly misses it.
267
+ // Mirrors `first_argument_value` in the Rust extractor for parity.
268
+ function firstStringArgument(node: TreeSitterNode): string | null {
235
269
  for (let i = 0; i < node.childCount; i++) {
236
270
  const child = node.child(i);
237
271
  if (!child || child.type !== 'arguments') continue;
@@ -239,15 +273,21 @@ function handleSetGeneric(node: TreeSitterNode, ctx: ExtractorOutput): void {
239
273
  const arg = child.child(j);
240
274
  if (!arg) continue;
241
275
  if (arg.type === 'string') {
242
- const name = arg.text.replace(/^["']|["']$/g, '');
243
- ctx.definitions.push({
244
- name,
245
- kind: 'function',
246
- line: node.startPosition.row + 1,
247
- endLine: nodeEndLine(node),
248
- });
249
- return;
276
+ return stripQuotes(arg.text);
277
+ }
278
+ if (arg.type === 'argument') {
279
+ const valueNode = arg.childForFieldName('value');
280
+ if (valueNode && valueNode.type === 'string') return stripQuotes(valueNode.text);
281
+ for (let k = 0; k < arg.childCount; k++) {
282
+ const inner = arg.child(k);
283
+ if (inner && inner.type === 'string') return stripQuotes(inner.text);
284
+ }
250
285
  }
251
286
  }
252
287
  }
288
+ return null;
289
+ }
290
+
291
+ function stripQuotes(text: string): string {
292
+ return text.replace(/^["']|["']$/g, '');
253
293
  }
@@ -1,5 +1,6 @@
1
1
  import type {
2
2
  Call,
3
+ Definition,
3
4
  ExtractorOutput,
4
5
  SubDeclaration,
5
6
  TreeSitterNode,
@@ -59,52 +60,37 @@ function walkScalaNode(node: TreeSitterNode, ctx: ExtractorOutput): void {
59
60
  // ── Walk-path per-node-type handlers ────────────────────────────────────────
60
61
 
61
62
  function handleScalaClassDef(node: TreeSitterNode, ctx: ExtractorOutput): void {
62
- const nameNode = node.childForFieldName('name');
63
- if (!nameNode) return;
64
- const name = nameNode.text;
65
- const children = extractScalaBodyMembers(node, name, ctx);
66
-
67
- ctx.definitions.push({
68
- name,
69
- kind: 'class',
70
- line: node.startPosition.row + 1,
71
- endLine: nodeEndLine(node),
72
- children: children.length > 0 ? children : undefined,
73
- });
74
-
75
- extractScalaInheritance(node, name, ctx);
63
+ emitScalaTypeDef(node, ctx, 'class');
76
64
  }
77
65
 
78
66
  function handleScalaTraitDef(node: TreeSitterNode, ctx: ExtractorOutput): void {
79
- const nameNode = node.childForFieldName('name');
80
- if (!nameNode) return;
81
- const name = nameNode.text;
82
- const children = extractScalaBodyMembers(node, name, ctx);
83
-
84
- ctx.definitions.push({
85
- name,
86
- kind: 'interface',
87
- line: node.startPosition.row + 1,
88
- endLine: nodeEndLine(node),
89
- children: children.length > 0 ? children : undefined,
90
- });
91
-
92
- extractScalaInheritance(node, name, ctx);
67
+ emitScalaTypeDef(node, ctx, 'interface');
93
68
  }
94
69
 
95
70
  function handleScalaObjectDef(node: TreeSitterNode, ctx: ExtractorOutput): void {
71
+ emitScalaTypeDef(node, ctx, 'class');
72
+ }
73
+
74
+ function emitScalaTypeDef(
75
+ node: TreeSitterNode,
76
+ ctx: ExtractorOutput,
77
+ kind: 'class' | 'interface',
78
+ ): void {
96
79
  const nameNode = node.childForFieldName('name');
97
80
  if (!nameNode) return;
98
81
  const name = nameNode.text;
99
- const children = extractScalaBodyMembers(node, name, ctx);
82
+ const { children, methods } = collectScalaBodyMembers(node, name);
100
83
 
84
+ // Push the type def before its methods so the definition order follows
85
+ // source-line order (matches native, which walks depth-first).
101
86
  ctx.definitions.push({
102
87
  name,
103
- kind: 'class',
88
+ kind,
104
89
  line: node.startPosition.row + 1,
105
90
  endLine: nodeEndLine(node),
106
91
  children: children.length > 0 ? children : undefined,
107
92
  });
93
+ for (const m of methods) ctx.definitions.push(m);
108
94
 
109
95
  extractScalaInheritance(node, name, ctx);
110
96
  }
@@ -224,14 +210,14 @@ function extractScalaInheritance(node: TreeSitterNode, name: string, ctx: Extrac
224
210
 
225
211
  // ── Body member extraction ──────────────────────────────────────────────────
226
212
 
227
- function extractScalaBodyMembers(
213
+ function collectScalaBodyMembers(
228
214
  parentNode: TreeSitterNode,
229
215
  parentName: string,
230
- ctx: ExtractorOutput,
231
- ): SubDeclaration[] {
216
+ ): { children: SubDeclaration[]; methods: Definition[] } {
232
217
  const children: SubDeclaration[] = [];
218
+ const methods: Definition[] = [];
233
219
  const body = findChild(parentNode, 'template_body');
234
- if (!body) return children;
220
+ if (!body) return { children, methods };
235
221
 
236
222
  for (let i = 0; i < body.childCount; i++) {
237
223
  const member = body.child(i);
@@ -240,12 +226,14 @@ function extractScalaBodyMembers(
240
226
  if (member.type === 'function_definition') {
241
227
  const methName = member.childForFieldName('name');
242
228
  if (methName) {
243
- ctx.definitions.push({
229
+ const params = extractScalaParameters(member);
230
+ methods.push({
244
231
  name: `${parentName}.${methName.text}`,
245
232
  kind: 'method',
246
233
  line: member.startPosition.row + 1,
247
234
  endLine: member.endPosition.row + 1,
248
235
  visibility: extractModifierVisibility(member),
236
+ children: params.length > 0 ? params : undefined,
249
237
  });
250
238
  }
251
239
  } else if (member.type === 'val_definition' || member.type === 'var_definition') {
@@ -264,7 +252,7 @@ function extractScalaBodyMembers(
264
252
  }
265
253
  }
266
254
 
267
- return children;
255
+ return { children, methods };
268
256
  }
269
257
 
270
258
  // ── Parameter extraction ────────────────────────────────────────────────────
@@ -156,15 +156,24 @@ function extractContractMember(child: TreeSitterNode): SubDeclaration | null {
156
156
  }
157
157
  }
158
158
 
159
- /** Extract inheritance (extends) relationships from a contract node. */
159
+ /**
160
+ * Extract inheritance (extends) relationships from a contract node.
161
+ *
162
+ * Each parent in `contract A is B, C, D { }` is its own `inheritance_specifier`
163
+ * sibling under the contract node (see tree-sitter-solidity grammar:
164
+ * `_class_heritage: "is" commaSep1($.inheritance_specifier)`), so we must walk
165
+ * all direct children rather than stopping at the first match.
166
+ */
160
167
  function extractInheritance(node: TreeSitterNode, name: string, ctx: ExtractorOutput): void {
161
- const inheritance = findChild(node, 'inheritance_specifier');
162
- if (!inheritance) return;
163
- for (let i = 0; i < inheritance.childCount; i++) {
164
- const child = inheritance.child(i);
165
- if (!child) continue;
166
- if (child.type === 'user_defined_type' || child.type === 'identifier') {
167
- ctx.classes.push({ name, extends: child.text, line: node.startPosition.row + 1 });
168
+ for (let i = 0; i < node.childCount; i++) {
169
+ const inheritance = node.child(i);
170
+ if (!inheritance || inheritance.type !== 'inheritance_specifier') continue;
171
+ for (let j = 0; j < inheritance.childCount; j++) {
172
+ const child = inheritance.child(j);
173
+ if (!child) continue;
174
+ if (child.type === 'user_defined_type' || child.type === 'identifier') {
175
+ ctx.classes.push({ name, extends: child.text, line: node.startPosition.row + 1 });
176
+ }
168
177
  }
169
178
  }
170
179
  }
@@ -99,27 +99,60 @@ function handlePackageDecl(node: TreeSitterNode, ctx: ExtractorOutput): void {
99
99
  }
100
100
 
101
101
  function handleClassDecl(node: TreeSitterNode, ctx: ExtractorOutput): void {
102
- const nameNode = node.childForFieldName('name');
103
- if (!nameNode) return;
102
+ // tree-sitter-verilog exposes no field names on `class_declaration`. The
103
+ // class name lives under a `class_identifier > simple_identifier` chain, and
104
+ // the superclass appears as a `class_type` child (no `superclass` field).
105
+ // The Rust extractor in `crates/codegraph-core/src/extractors/verilog.rs`
106
+ // uses the same structural lookups so both engines emit identical class
107
+ // definitions and `extends` relations.
108
+ const name = findClassName(node);
109
+ if (!name) return;
104
110
 
105
111
  ctx.definitions.push({
106
- name: nameNode.text,
112
+ name,
107
113
  kind: 'class',
108
114
  line: node.startPosition.row + 1,
109
115
  endLine: nodeEndLine(node),
110
116
  });
111
117
 
112
- // Superclass via extends
113
- const superclass = node.childForFieldName('superclass');
118
+ const superclass = findClassSuperclass(node);
114
119
  if (superclass) {
115
120
  ctx.classes.push({
116
- name: nameNode.text,
117
- extends: superclass.text,
121
+ name,
122
+ extends: superclass,
118
123
  line: node.startPosition.row + 1,
119
124
  });
120
125
  }
121
126
  }
122
127
 
128
+ function findClassName(node: TreeSitterNode): string | null {
129
+ const fieldName = node.childForFieldName('name');
130
+ if (fieldName) return fieldName.text;
131
+ for (let i = 0; i < node.childCount; i++) {
132
+ const child = node.child(i);
133
+ if (child && child.type === 'class_identifier') {
134
+ const simple = findChild(child, 'simple_identifier');
135
+ return (simple ?? child).text.trim();
136
+ }
137
+ }
138
+ return null;
139
+ }
140
+
141
+ function findClassSuperclass(node: TreeSitterNode): string | null {
142
+ for (let i = 0; i < node.childCount; i++) {
143
+ const child = node.child(i);
144
+ if (child && child.type === 'class_type') {
145
+ const id = findChild(child, 'class_identifier');
146
+ if (id) {
147
+ const simple = findChild(id, 'simple_identifier');
148
+ return (simple ?? id).text.trim();
149
+ }
150
+ return child.text.trim();
151
+ }
152
+ }
153
+ return null;
154
+ }
155
+
123
156
  function handleFunctionDecl(node: TreeSitterNode, ctx: ExtractorOutput): void {
124
157
  const nameNode = findFunctionOrTaskName(node, 'function_identifier');
125
158
  if (!nameNode) return;
@@ -151,8 +184,12 @@ function handleTaskDecl(node: TreeSitterNode, ctx: ExtractorOutput): void {
151
184
  }
152
185
 
153
186
  function handleModuleInstantiation(node: TreeSitterNode, ctx: ExtractorOutput): void {
154
- // Module instantiations are like function calls: `ModuleName instance_name(...);`
155
- const moduleType = node.childForFieldName('type') || node.child(0);
187
+ // Module instantiations are like function calls: `ModuleName instance_name(...);`.
188
+ // The module type identifier is the first *named* child; using
189
+ // `namedChild(0)` (instead of `child(0)`) skips anonymous tokens like a
190
+ // leading `#` parameter-override punctuation so we never capture that as a
191
+ // call name. The Rust extractor uses the same lookup for parity.
192
+ const moduleType = node.childForFieldName('type') ?? node.namedChild(0);
156
193
  if (!moduleType) return;
157
194
 
158
195
  ctx.calls.push({
@@ -169,7 +206,10 @@ function handlePackageImport(node: TreeSitterNode, ctx: ExtractorOutput): void {
169
206
  if (child.type === 'package_import_item') {
170
207
  const text = child.text;
171
208
  const parts = text.split('::');
172
- const pkg = parts[0] ?? text;
209
+ // `String.split('::')` always yields at least one element — when the
210
+ // delimiter is absent the whole string is the sole item, so the
211
+ // empty-string fallback is unreachable in practice.
212
+ const pkg = parts[0] ?? '';
173
213
  const item = parts[1] ?? '*';
174
214
  ctx.imports.push({
175
215
  source: pkg,
@@ -182,9 +222,18 @@ function handlePackageImport(node: TreeSitterNode, ctx: ExtractorOutput): void {
182
222
 
183
223
  function handleIncludeDirective(node: TreeSitterNode, ctx: ExtractorOutput): void {
184
224
  // `include "file.vh"
225
+ // Mirrors the Rust `handle_include_directive` which checks all three node
226
+ // kinds — tree-sitter-verilog has emitted `double_quoted_string` in some
227
+ // grammar revisions, and missing it would silently drop the import in WASM
228
+ // while the native engine still records it.
185
229
  for (let i = 0; i < node.childCount; i++) {
186
230
  const child = node.child(i);
187
- if (child && (child.type === 'string_literal' || child.type === 'quoted_string')) {
231
+ if (
232
+ child &&
233
+ (child.type === 'string_literal' ||
234
+ child.type === 'quoted_string' ||
235
+ child.type === 'double_quoted_string')
236
+ ) {
188
237
  const source = child.text.replace(/^["']|["']$/g, '');
189
238
  ctx.imports.push({
190
239
  source,
@@ -266,8 +315,14 @@ function findVerilogParent(node: TreeSitterNode): string | null {
266
315
  current.type === 'package_declaration' ||
267
316
  current.type === 'class_declaration'
268
317
  ) {
269
- const name = findDeclName(current) || findModuleName(current);
270
- return name ? name.text : null;
318
+ // `class_declaration` wraps its name in `class_identifier >
319
+ // simple_identifier`; `findDeclName` / `findModuleName` only look at
320
+ // bare `simple_identifier`/`identifier` children, so they miss it.
321
+ // `findClassName` already handles the wrapper, so consult it last to
322
+ // qualify tasks/functions nested inside a SystemVerilog class.
323
+ const nameNode = findDeclName(current) || findModuleName(current);
324
+ if (nameNode) return nameNode.text;
325
+ return findClassName(current);
271
326
  }
272
327
  current = current.parent;
273
328
  }
@@ -292,17 +347,30 @@ function extractPorts(moduleNode: TreeSitterNode): SubDeclaration[] {
292
347
  ) {
293
348
  const nameNode =
294
349
  child.childForFieldName('name') ||
350
+ findChild(child, 'port_identifier') ||
295
351
  findChild(child, 'simple_identifier') ||
296
352
  findChild(child, 'identifier');
297
353
  if (nameNode) {
298
- ports.push({ name: nameNode.text, kind: 'property', line: child.startPosition.row + 1 });
354
+ // `port_identifier` wraps a `simple_identifier`; descend to the
355
+ // innermost identifier for a clean, whitespace-free name.
356
+ const inner =
357
+ findChild(nameNode, 'simple_identifier') ||
358
+ findChild(nameNode, 'identifier') ||
359
+ nameNode;
360
+ ports.push({ name: inner.text, kind: 'property', line: child.startPosition.row + 1 });
299
361
  }
300
362
  }
301
363
 
302
- // Recurse into port list containers
364
+ // Recurse into port list containers. `module_ansi_header` wraps the
365
+ // ANSI-style declarations emitted by tree-sitter-verilog (e.g.
366
+ // `module top(input clk, output reg q);`) — without this branch the
367
+ // WASM engine returns an empty children array while the native engine
368
+ // (which includes the same kind in its CONTAINER_KINDS list) returns
369
+ // the correct ports, breaking engine parity.
303
370
  if (
304
371
  child.type === 'list_of_port_declarations' ||
305
372
  child.type === 'module_header' ||
373
+ child.type === 'module_ansi_header' ||
306
374
  child.type === 'port_declaration_list'
307
375
  ) {
308
376
  collectFromNode(child);
@@ -147,6 +147,7 @@ export const DEFAULTS = {
147
147
  implementations: 50,
148
148
  interfaces: 50,
149
149
  },
150
+ disabledTools: [] as string[],
150
151
  },
151
152
  } satisfies CodegraphConfig;
152
153
 
package/src/mcp/server.ts CHANGED
@@ -98,17 +98,15 @@ async function resolveDbPath(
98
98
  return dbPath;
99
99
  }
100
100
 
101
- function validateMultiRepoAccess(multiRepo: boolean, name: string, args: { repo?: string }): void {
101
+ function validateMultiRepoAccess(multiRepo: boolean, args: { repo?: string }): void {
102
102
  if (!multiRepo && args.repo) {
103
103
  throw new ConfigError(
104
104
  'Multi-repo access is disabled. Restart with `codegraph mcp --multi-repo` to access other repositories.',
105
105
  );
106
106
  }
107
- if (!multiRepo && name === 'list_repos') {
108
- throw new ConfigError(
109
- 'Multi-repo access is disabled. Restart with `codegraph mcp --multi-repo` to list repositories.',
110
- );
111
- }
107
+ // Note: the `list_repos` tool is excluded from `enabledToolNames` when
108
+ // `multiRepo` is false (see `buildToolList`), so any call to it is rejected
109
+ // earlier in `createCallToolHandler` with an "Unknown tool" error.
112
110
  }
113
111
 
114
112
  /**
@@ -163,11 +161,17 @@ function createCallToolHandler(
163
161
  customDbPath: string | undefined,
164
162
  allowedRepos: string[] | undefined,
165
163
  getQueries: () => Promise<unknown>,
164
+ enabledToolNames: Set<string>,
166
165
  ) {
167
166
  return async (request: any) => {
168
167
  const { name, arguments: args } = request.params;
169
168
  try {
170
- validateMultiRepoAccess(multiRepo, name, args);
169
+ if (!enabledToolNames.has(name)) {
170
+ return { content: [{ type: 'text', text: `Unknown tool: ${name}` }], isError: true };
171
+ }
172
+
173
+ validateMultiRepoAccess(multiRepo, args);
174
+
171
175
  const dbPath = await resolveDbPath(customDbPath, args, allowedRepos);
172
176
 
173
177
  const toolEntry = TOOL_HANDLERS.get(name);
@@ -209,6 +213,9 @@ export async function startMCPServer(
209
213
  // Apply config-based MCP page-size overrides
210
214
  const config = options.config || loadConfig();
211
215
  initMcpDefaults(config.mcp?.defaults ? { ...config.mcp.defaults } : undefined);
216
+ const disabledTools = [...(config.mcp?.disabledTools ?? [])];
217
+ const enabledTools = buildToolList(multiRepo, disabledTools);
218
+ const enabledToolNames = new Set(enabledTools.map((tool) => tool.name));
212
219
 
213
220
  const { Server, StdioServerTransport, ListToolsRequestSchema, CallToolRequestSchema } =
214
221
  await loadMCPSdk();
@@ -225,12 +232,12 @@ export async function startMCPServer(
225
232
  );
226
233
 
227
234
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
228
- tools: buildToolList(multiRepo),
235
+ tools: enabledTools,
229
236
  }));
230
237
 
231
238
  server.setRequestHandler(
232
239
  CallToolRequestSchema,
233
- createCallToolHandler(multiRepo, customDbPath, allowedRepos, getQueries),
240
+ createCallToolHandler(multiRepo, customDbPath, allowedRepos, getQueries, enabledToolNames),
234
241
  );
235
242
 
236
243
  const transport = new (StdioServerTransport as any)();
@@ -29,6 +29,17 @@ const PAGINATION_PROPS: Record<string, unknown> = {
29
29
  offset: { type: 'number', description: 'Skip this many results (pagination, default: 0)' },
30
30
  };
31
31
 
32
+ function normalizeToolName(name: string): string {
33
+ return name
34
+ .trim()
35
+ .toLowerCase()
36
+ .replace(/^codegraph\d+_/, '');
37
+ }
38
+
39
+ function buildDisabledToolSet(disabledTools?: string[]): Set<string> {
40
+ return new Set((disabledTools || []).map((name) => normalizeToolName(name)).filter(Boolean));
41
+ }
42
+
32
43
  const BASE_TOOLS: ToolSchema[] = [
33
44
  {
34
45
  name: 'query',
@@ -311,6 +322,11 @@ const BASE_TOOLS: ToolSchema[] = [
311
322
  description:
312
323
  'Search mode: hybrid (BM25 + semantic, default), semantic (embeddings only), keyword (BM25 only)',
313
324
  },
325
+ file_pattern: {
326
+ oneOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' } }],
327
+ description:
328
+ 'Restrict results to files matching one or more glob or substring patterns (e.g. "db/", "src/**/*.ts", or ["db/", "src/"])',
329
+ },
314
330
  ...PAGINATION_PROPS,
315
331
  },
316
332
  required: ['query'],
@@ -849,18 +865,25 @@ const LIST_REPOS_TOOL: ToolSchema = {
849
865
  /**
850
866
  * Build the tool list based on multi-repo mode.
851
867
  */
852
- export function buildToolList(multiRepo: boolean): ToolSchema[] {
853
- if (!multiRepo) return BASE_TOOLS;
854
- return [
855
- ...BASE_TOOLS.map((tool) => ({
868
+ export function buildToolList(multiRepo: boolean, disabledTools?: string[]): ToolSchema[] {
869
+ const disabled = buildDisabledToolSet(disabledTools);
870
+ const includeTool = (tool: ToolSchema): boolean => !disabled.has(normalizeToolName(tool.name));
871
+ const baseTools = BASE_TOOLS.filter(includeTool);
872
+
873
+ if (!multiRepo) return baseTools;
874
+
875
+ const tools: ToolSchema[] = [
876
+ ...baseTools.map((tool) => ({
856
877
  ...tool,
857
878
  inputSchema: {
858
879
  ...tool.inputSchema,
859
880
  properties: { ...tool.inputSchema.properties, ...REPO_PROP },
860
881
  },
861
882
  })),
862
- LIST_REPOS_TOOL,
863
883
  ];
884
+
885
+ if (includeTool(LIST_REPOS_TOOL)) tools.push(LIST_REPOS_TOOL);
886
+ return tools;
864
887
  }
865
888
 
866
889
  // Backward-compatible export: full multi-repo tool list