@optave/codegraph 3.10.0 → 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 (139) hide show
  1. package/README.md +13 -13
  2. package/dist/ast-analysis/rules/index.d.ts.map +1 -1
  3. package/dist/ast-analysis/rules/index.js +77 -0
  4. package/dist/ast-analysis/rules/index.js.map +1 -1
  5. package/dist/cli/commands/audit.js +1 -1
  6. package/dist/cli/commands/audit.js.map +1 -1
  7. package/dist/cli/commands/build.d.ts.map +1 -1
  8. package/dist/cli/commands/build.js +2 -0
  9. package/dist/cli/commands/build.js.map +1 -1
  10. package/dist/cli/commands/check.js +1 -1
  11. package/dist/cli/commands/check.js.map +1 -1
  12. package/dist/cli/commands/children.js +1 -1
  13. package/dist/cli/commands/children.js.map +1 -1
  14. package/dist/cli/commands/diff-impact.js +1 -1
  15. package/dist/cli/commands/diff-impact.js.map +1 -1
  16. package/dist/cli/commands/roles.js +1 -1
  17. package/dist/cli/commands/roles.js.map +1 -1
  18. package/dist/cli/commands/structure.js +1 -1
  19. package/dist/cli/commands/structure.js.map +1 -1
  20. package/dist/cli/shared/options.js +1 -1
  21. package/dist/cli/shared/options.js.map +1 -1
  22. package/dist/db/connection.d.ts.map +1 -1
  23. package/dist/db/connection.js +8 -0
  24. package/dist/db/connection.js.map +1 -1
  25. package/dist/domain/graph/builder/incremental.d.ts +0 -6
  26. package/dist/domain/graph/builder/incremental.d.ts.map +1 -1
  27. package/dist/domain/graph/builder/incremental.js +6 -23
  28. package/dist/domain/graph/builder/incremental.js.map +1 -1
  29. package/dist/domain/graph/builder/pipeline.d.ts +44 -0
  30. package/dist/domain/graph/builder/pipeline.d.ts.map +1 -1
  31. package/dist/domain/graph/builder/pipeline.js +181 -39
  32. package/dist/domain/graph/builder/pipeline.js.map +1 -1
  33. package/dist/domain/graph/builder/stages/build-edges.d.ts.map +1 -1
  34. package/dist/domain/graph/builder/stages/build-edges.js +8 -2
  35. package/dist/domain/graph/builder/stages/build-edges.js.map +1 -1
  36. package/dist/domain/graph/builder/stages/resolve-imports.d.ts.map +1 -1
  37. package/dist/domain/graph/builder/stages/resolve-imports.js +73 -22
  38. package/dist/domain/graph/builder/stages/resolve-imports.js.map +1 -1
  39. package/dist/domain/graph/watcher.d.ts.map +1 -1
  40. package/dist/domain/graph/watcher.js +23 -18
  41. package/dist/domain/graph/watcher.js.map +1 -1
  42. package/dist/domain/parser.d.ts.map +1 -1
  43. package/dist/domain/parser.js +27 -1
  44. package/dist/domain/parser.js.map +1 -1
  45. package/dist/domain/search/models.d.ts +16 -0
  46. package/dist/domain/search/models.d.ts.map +1 -1
  47. package/dist/domain/search/models.js +35 -1
  48. package/dist/domain/search/models.js.map +1 -1
  49. package/dist/domain/wasm-worker-entry.js +8 -1
  50. package/dist/domain/wasm-worker-entry.js.map +1 -1
  51. package/dist/extractors/c.js +25 -6
  52. package/dist/extractors/c.js.map +1 -1
  53. package/dist/extractors/cpp.js +47 -6
  54. package/dist/extractors/cpp.js.map +1 -1
  55. package/dist/extractors/cuda.js +90 -14
  56. package/dist/extractors/cuda.js.map +1 -1
  57. package/dist/extractors/elixir.js +83 -3
  58. package/dist/extractors/elixir.js.map +1 -1
  59. package/dist/extractors/erlang.js +56 -20
  60. package/dist/extractors/erlang.js.map +1 -1
  61. package/dist/extractors/fsharp.d.ts +7 -0
  62. package/dist/extractors/fsharp.d.ts.map +1 -1
  63. package/dist/extractors/fsharp.js +94 -0
  64. package/dist/extractors/fsharp.js.map +1 -1
  65. package/dist/extractors/gleam.js +6 -2
  66. package/dist/extractors/gleam.js.map +1 -1
  67. package/dist/extractors/groovy.js +41 -1
  68. package/dist/extractors/groovy.js.map +1 -1
  69. package/dist/extractors/haskell.js +48 -4
  70. package/dist/extractors/haskell.js.map +1 -1
  71. package/dist/extractors/julia.js +172 -41
  72. package/dist/extractors/julia.js.map +1 -1
  73. package/dist/extractors/kotlin.js +4 -0
  74. package/dist/extractors/kotlin.js.map +1 -1
  75. package/dist/extractors/objc.js +184 -47
  76. package/dist/extractors/objc.js.map +1 -1
  77. package/dist/extractors/python.js +7 -4
  78. package/dist/extractors/python.js.map +1 -1
  79. package/dist/extractors/r.js +93 -52
  80. package/dist/extractors/r.js.map +1 -1
  81. package/dist/extractors/scala.d.ts.map +1 -1
  82. package/dist/extractors/scala.js +18 -32
  83. package/dist/extractors/scala.js.map +1 -1
  84. package/dist/extractors/solidity.js +18 -9
  85. package/dist/extractors/solidity.js.map +1 -1
  86. package/dist/extractors/verilog.js +80 -15
  87. package/dist/extractors/verilog.js.map +1 -1
  88. package/dist/mcp/tool-registry.d.ts.map +1 -1
  89. package/dist/mcp/tool-registry.js +4 -0
  90. package/dist/mcp/tool-registry.js.map +1 -1
  91. package/dist/mcp/tools/semantic-search.d.ts +1 -0
  92. package/dist/mcp/tools/semantic-search.d.ts.map +1 -1
  93. package/dist/mcp/tools/semantic-search.js +1 -0
  94. package/dist/mcp/tools/semantic-search.js.map +1 -1
  95. package/dist/types.d.ts +15 -1
  96. package/dist/types.d.ts.map +1 -1
  97. package/grammars/tree-sitter-erlang.wasm +0 -0
  98. package/grammars/tree-sitter-fsharp.wasm +0 -0
  99. package/grammars/tree-sitter-fsharp_signature.wasm +0 -0
  100. package/grammars/tree-sitter-gleam.wasm +0 -0
  101. package/package.json +10 -10
  102. package/src/ast-analysis/rules/index.ts +87 -0
  103. package/src/cli/commands/audit.ts +1 -1
  104. package/src/cli/commands/build.ts +2 -0
  105. package/src/cli/commands/check.ts +1 -1
  106. package/src/cli/commands/children.ts +1 -1
  107. package/src/cli/commands/diff-impact.ts +1 -1
  108. package/src/cli/commands/roles.ts +1 -1
  109. package/src/cli/commands/structure.ts +1 -1
  110. package/src/cli/shared/options.ts +1 -1
  111. package/src/db/connection.ts +8 -0
  112. package/src/domain/graph/builder/incremental.ts +6 -41
  113. package/src/domain/graph/builder/pipeline.ts +222 -37
  114. package/src/domain/graph/builder/stages/build-edges.ts +9 -2
  115. package/src/domain/graph/builder/stages/resolve-imports.ts +79 -25
  116. package/src/domain/graph/watcher.ts +21 -23
  117. package/src/domain/parser.ts +27 -1
  118. package/src/domain/search/models.ts +36 -1
  119. package/src/domain/wasm-worker-entry.ts +8 -1
  120. package/src/extractors/c.ts +27 -8
  121. package/src/extractors/cpp.ts +50 -8
  122. package/src/extractors/cuda.ts +90 -16
  123. package/src/extractors/elixir.ts +75 -3
  124. package/src/extractors/erlang.ts +63 -20
  125. package/src/extractors/fsharp.ts +104 -0
  126. package/src/extractors/gleam.ts +7 -2
  127. package/src/extractors/groovy.ts +45 -1
  128. package/src/extractors/haskell.ts +45 -4
  129. package/src/extractors/julia.ts +164 -43
  130. package/src/extractors/kotlin.ts +4 -0
  131. package/src/extractors/objc.ts +171 -47
  132. package/src/extractors/python.ts +5 -3
  133. package/src/extractors/r.ts +88 -48
  134. package/src/extractors/scala.ts +24 -36
  135. package/src/extractors/solidity.ts +17 -8
  136. package/src/extractors/verilog.ts +83 -15
  137. package/src/mcp/tool-registry.ts +5 -0
  138. package/src/mcp/tools/semantic-search.ts +2 -0
  139. package/src/types.ts +15 -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);
@@ -322,6 +322,11 @@ const BASE_TOOLS: ToolSchema[] = [
322
322
  description:
323
323
  'Search mode: hybrid (BM25 + semantic, default), semantic (embeddings only), keyword (BM25 only)',
324
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
+ },
325
330
  ...PAGINATION_PROPS,
326
331
  },
327
332
  required: ['query'],
@@ -9,6 +9,7 @@ interface SemanticSearchArgs {
9
9
  limit?: number;
10
10
  offset?: number;
11
11
  min_score?: number;
12
+ file_pattern?: string | string[];
12
13
  }
13
14
 
14
15
  export async function handler(args: SemanticSearchArgs, ctx: McpToolContext): Promise<unknown> {
@@ -17,6 +18,7 @@ export async function handler(args: SemanticSearchArgs, ctx: McpToolContext): Pr
17
18
  limit: Math.min(args.limit ?? MCP_DEFAULTS.semantic_search ?? 100, ctx.MCP_MAX_LIMIT),
18
19
  offset: effectiveOffset(args),
19
20
  minScore: args.min_score,
21
+ filePattern: args.file_pattern,
20
22
  };
21
23
 
22
24
  if (mode === 'keyword') {
package/src/types.ts CHANGED
@@ -99,6 +99,7 @@ export type LanguageId =
99
99
  | 'ocaml'
100
100
  | 'ocaml-interface'
101
101
  | 'fsharp'
102
+ | 'fsharp-signature'
102
103
  | 'gleam'
103
104
  | 'clojure'
104
105
  | 'julia'
@@ -576,6 +577,7 @@ export interface TreeSitterNode {
576
577
  child(index: number): TreeSitterNode | null;
577
578
  namedChild(index: number): TreeSitterNode | null;
578
579
  childForFieldName(name: string): TreeSitterNode | null;
580
+ fieldNameForChild(index: number): string | null;
579
581
  parent: TreeSitterNode | null;
580
582
  previousSibling: TreeSitterNode | null;
581
583
  nextSibling: TreeSitterNode | null;
@@ -1062,7 +1064,20 @@ export interface BuildGraphOpts {
1062
1064
  complexity?: boolean;
1063
1065
  cfg?: boolean;
1064
1066
  scope?: string[];
1067
+ /**
1068
+ * Glob patterns merged on top of `config.exclude` for this build only.
1069
+ * Lets callers extend exclusion programmatically without writing a config
1070
+ * file — used by the benchmark scripts to skip resolution-benchmark
1071
+ * fixtures that aren't representative of real code.
1072
+ */
1073
+ exclude?: string[];
1065
1074
  skipRegistry?: boolean;
1075
+ /**
1076
+ * Override the graph.db location. Resolved absolute. When omitted, the
1077
+ * pipeline writes to `<rootDir>/.codegraph/graph.db` — same default as
1078
+ * `findDbPath` for every other DB-scoped command.
1079
+ */
1080
+ dbPath?: string;
1066
1081
  }
1067
1082
 
1068
1083
  /** Build timing result from buildGraph. */