@optave/codegraph 3.6.0 → 3.8.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 +32 -16
  2. package/dist/ast-analysis/engine.d.ts.map +1 -1
  3. package/dist/ast-analysis/engine.js +158 -1
  4. package/dist/ast-analysis/engine.js.map +1 -1
  5. package/dist/ast-analysis/rules/javascript.d.ts.map +1 -1
  6. package/dist/ast-analysis/rules/javascript.js +0 -1
  7. package/dist/ast-analysis/rules/javascript.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 +2 -75
  10. package/dist/ast-analysis/visitors/ast-store-visitor.js.map +1 -1
  11. package/dist/cli/commands/ast.js +2 -2
  12. package/dist/cli/commands/ast.js.map +1 -1
  13. package/dist/domain/graph/builder/pipeline.d.ts.map +1 -1
  14. package/dist/domain/graph/builder/pipeline.js +128 -6
  15. package/dist/domain/graph/builder/pipeline.js.map +1 -1
  16. package/dist/domain/graph/builder/stages/build-edges.d.ts.map +1 -1
  17. package/dist/domain/graph/builder/stages/build-edges.js +101 -1
  18. package/dist/domain/graph/builder/stages/build-edges.js.map +1 -1
  19. package/dist/domain/graph/builder/stages/collect-files.d.ts.map +1 -1
  20. package/dist/domain/graph/builder/stages/collect-files.js +17 -5
  21. package/dist/domain/graph/builder/stages/collect-files.js.map +1 -1
  22. package/dist/domain/graph/builder/stages/detect-changes.d.ts.map +1 -1
  23. package/dist/domain/graph/builder/stages/detect-changes.js +98 -50
  24. package/dist/domain/graph/builder/stages/detect-changes.js.map +1 -1
  25. package/dist/domain/graph/builder/stages/finalize.d.ts.map +1 -1
  26. package/dist/domain/graph/builder/stages/finalize.js +32 -5
  27. package/dist/domain/graph/builder/stages/finalize.js.map +1 -1
  28. package/dist/domain/graph/builder/stages/insert-nodes.d.ts.map +1 -1
  29. package/dist/domain/graph/builder/stages/insert-nodes.js +20 -7
  30. package/dist/domain/graph/builder/stages/insert-nodes.js.map +1 -1
  31. package/dist/domain/parser.d.ts +1 -1
  32. package/dist/domain/parser.d.ts.map +1 -1
  33. package/dist/domain/parser.js +129 -3
  34. package/dist/domain/parser.js.map +1 -1
  35. package/dist/extractors/clojure.d.ts +12 -0
  36. package/dist/extractors/clojure.d.ts.map +1 -0
  37. package/dist/extractors/clojure.js +245 -0
  38. package/dist/extractors/clojure.js.map +1 -0
  39. package/dist/extractors/cuda.d.ts +11 -0
  40. package/dist/extractors/cuda.d.ts.map +1 -0
  41. package/dist/extractors/cuda.js +302 -0
  42. package/dist/extractors/cuda.js.map +1 -0
  43. package/dist/extractors/dart.d.ts +6 -0
  44. package/dist/extractors/dart.d.ts.map +1 -0
  45. package/dist/extractors/dart.js +277 -0
  46. package/dist/extractors/dart.js.map +1 -0
  47. package/dist/extractors/elixir.d.ts +9 -0
  48. package/dist/extractors/elixir.d.ts.map +1 -0
  49. package/dist/extractors/elixir.js +223 -0
  50. package/dist/extractors/elixir.js.map +1 -0
  51. package/dist/extractors/erlang.d.ts +14 -0
  52. package/dist/extractors/erlang.d.ts.map +1 -0
  53. package/dist/extractors/erlang.js +239 -0
  54. package/dist/extractors/erlang.js.map +1 -0
  55. package/dist/extractors/fsharp.d.ts +13 -0
  56. package/dist/extractors/fsharp.d.ts.map +1 -0
  57. package/dist/extractors/fsharp.js +218 -0
  58. package/dist/extractors/fsharp.js.map +1 -0
  59. package/dist/extractors/gleam.d.ts +14 -0
  60. package/dist/extractors/gleam.d.ts.map +1 -0
  61. package/dist/extractors/gleam.js +229 -0
  62. package/dist/extractors/gleam.js.map +1 -0
  63. package/dist/extractors/groovy.d.ts +10 -0
  64. package/dist/extractors/groovy.d.ts.map +1 -0
  65. package/dist/extractors/groovy.js +304 -0
  66. package/dist/extractors/groovy.js.map +1 -0
  67. package/dist/extractors/haskell.d.ts +8 -0
  68. package/dist/extractors/haskell.d.ts.map +1 -0
  69. package/dist/extractors/haskell.js +217 -0
  70. package/dist/extractors/haskell.js.map +1 -0
  71. package/dist/extractors/index.d.ts +17 -0
  72. package/dist/extractors/index.d.ts.map +1 -1
  73. package/dist/extractors/index.js +17 -0
  74. package/dist/extractors/index.js.map +1 -1
  75. package/dist/extractors/julia.d.ts +16 -0
  76. package/dist/extractors/julia.d.ts.map +1 -0
  77. package/dist/extractors/julia.js +287 -0
  78. package/dist/extractors/julia.js.map +1 -0
  79. package/dist/extractors/lua.d.ts +6 -0
  80. package/dist/extractors/lua.d.ts.map +1 -0
  81. package/dist/extractors/lua.js +162 -0
  82. package/dist/extractors/lua.js.map +1 -0
  83. package/dist/extractors/objc.d.ts +9 -0
  84. package/dist/extractors/objc.d.ts.map +1 -0
  85. package/dist/extractors/objc.js +406 -0
  86. package/dist/extractors/objc.js.map +1 -0
  87. package/dist/extractors/ocaml.d.ts +6 -0
  88. package/dist/extractors/ocaml.d.ts.map +1 -0
  89. package/dist/extractors/ocaml.js +310 -0
  90. package/dist/extractors/ocaml.js.map +1 -0
  91. package/dist/extractors/r.d.ts +13 -0
  92. package/dist/extractors/r.d.ts.map +1 -0
  93. package/dist/extractors/r.js +251 -0
  94. package/dist/extractors/r.js.map +1 -0
  95. package/dist/extractors/solidity.d.ts +9 -0
  96. package/dist/extractors/solidity.d.ts.map +1 -0
  97. package/dist/extractors/solidity.js +374 -0
  98. package/dist/extractors/solidity.js.map +1 -0
  99. package/dist/extractors/verilog.d.ts +9 -0
  100. package/dist/extractors/verilog.d.ts.map +1 -0
  101. package/dist/extractors/verilog.js +286 -0
  102. package/dist/extractors/verilog.js.map +1 -0
  103. package/dist/extractors/zig.d.ts +9 -0
  104. package/dist/extractors/zig.d.ts.map +1 -0
  105. package/dist/extractors/zig.js +276 -0
  106. package/dist/extractors/zig.js.map +1 -0
  107. package/dist/features/ast.d.ts.map +1 -1
  108. package/dist/features/ast.js +1 -2
  109. package/dist/features/ast.js.map +1 -1
  110. package/dist/features/cfg.d.ts +1 -1
  111. package/dist/features/cfg.d.ts.map +1 -1
  112. package/dist/features/cfg.js +6 -51
  113. package/dist/features/cfg.js.map +1 -1
  114. package/dist/graph/algorithms/bfs.d.ts +2 -0
  115. package/dist/graph/algorithms/bfs.d.ts.map +1 -1
  116. package/dist/graph/algorithms/bfs.js +27 -0
  117. package/dist/graph/algorithms/bfs.js.map +1 -1
  118. package/dist/graph/algorithms/centrality.d.ts +2 -0
  119. package/dist/graph/algorithms/centrality.d.ts.map +1 -1
  120. package/dist/graph/algorithms/centrality.js +28 -0
  121. package/dist/graph/algorithms/centrality.js.map +1 -1
  122. package/dist/graph/algorithms/louvain.d.ts +3 -4
  123. package/dist/graph/algorithms/louvain.d.ts.map +1 -1
  124. package/dist/graph/algorithms/louvain.js +29 -0
  125. package/dist/graph/algorithms/louvain.js.map +1 -1
  126. package/dist/graph/algorithms/shortest-path.d.ts +2 -0
  127. package/dist/graph/algorithms/shortest-path.d.ts.map +1 -1
  128. package/dist/graph/algorithms/shortest-path.js +18 -1
  129. package/dist/graph/algorithms/shortest-path.js.map +1 -1
  130. package/dist/types.d.ts +122 -2
  131. package/dist/types.d.ts.map +1 -1
  132. package/grammars/tree-sitter-clojure.wasm +0 -0
  133. package/grammars/tree-sitter-cuda.wasm +0 -0
  134. package/grammars/tree-sitter-dart.wasm +0 -0
  135. package/grammars/tree-sitter-elixir.wasm +0 -0
  136. package/grammars/tree-sitter-erlang.wasm +0 -0
  137. package/grammars/tree-sitter-fsharp.wasm +0 -0
  138. package/grammars/tree-sitter-gleam.wasm +0 -0
  139. package/grammars/tree-sitter-groovy.wasm +0 -0
  140. package/grammars/tree-sitter-haskell.wasm +0 -0
  141. package/grammars/tree-sitter-julia.wasm +0 -0
  142. package/grammars/tree-sitter-lua.wasm +0 -0
  143. package/grammars/tree-sitter-objc.wasm +0 -0
  144. package/grammars/tree-sitter-ocaml.wasm +0 -0
  145. package/grammars/tree-sitter-ocaml_interface.wasm +0 -0
  146. package/grammars/tree-sitter-r.wasm +0 -0
  147. package/grammars/tree-sitter-solidity.wasm +0 -0
  148. package/grammars/tree-sitter-verilog.wasm +0 -0
  149. package/grammars/tree-sitter-zig.wasm +0 -0
  150. package/package.json +24 -7
  151. package/src/ast-analysis/engine.ts +183 -1
  152. package/src/ast-analysis/rules/javascript.ts +0 -1
  153. package/src/ast-analysis/visitors/ast-store-visitor.ts +2 -75
  154. package/src/cli/commands/ast.ts +2 -2
  155. package/src/domain/graph/builder/pipeline.ts +142 -6
  156. package/src/domain/graph/builder/stages/build-edges.ts +158 -1
  157. package/src/domain/graph/builder/stages/collect-files.ts +18 -7
  158. package/src/domain/graph/builder/stages/detect-changes.ts +109 -55
  159. package/src/domain/graph/builder/stages/finalize.ts +39 -9
  160. package/src/domain/graph/builder/stages/insert-nodes.ts +18 -7
  161. package/src/domain/parser.ts +161 -1
  162. package/src/extractors/clojure.ts +273 -0
  163. package/src/extractors/cuda.ts +316 -0
  164. package/src/extractors/dart.ts +304 -0
  165. package/src/extractors/elixir.ts +251 -0
  166. package/src/extractors/erlang.ts +252 -0
  167. package/src/extractors/fsharp.ts +253 -0
  168. package/src/extractors/gleam.ts +246 -0
  169. package/src/extractors/groovy.ts +332 -0
  170. package/src/extractors/haskell.ts +235 -0
  171. package/src/extractors/index.ts +17 -0
  172. package/src/extractors/julia.ts +318 -0
  173. package/src/extractors/lua.ts +169 -0
  174. package/src/extractors/objc.ts +431 -0
  175. package/src/extractors/ocaml.ts +337 -0
  176. package/src/extractors/r.ts +253 -0
  177. package/src/extractors/solidity.ts +398 -0
  178. package/src/extractors/verilog.ts +315 -0
  179. package/src/extractors/zig.ts +294 -0
  180. package/src/features/ast.ts +1 -2
  181. package/src/features/cfg.ts +6 -51
  182. package/src/graph/algorithms/bfs.ts +34 -0
  183. package/src/graph/algorithms/centrality.ts +30 -0
  184. package/src/graph/algorithms/louvain.ts +31 -4
  185. package/src/graph/algorithms/shortest-path.ts +20 -1
  186. package/src/types.ts +123 -2
@@ -0,0 +1,294 @@
1
+ import type {
2
+ Call,
3
+ ExtractorOutput,
4
+ SubDeclaration,
5
+ TreeSitterNode,
6
+ TreeSitterTree,
7
+ } from '../types.js';
8
+ import { findChild, nodeEndLine } from './helpers.js';
9
+
10
+ /**
11
+ * Extract symbols from Zig files.
12
+ *
13
+ * Zig's structs/enums/unions are anonymous — their names come from the
14
+ * enclosing `variable_declaration` (e.g. `const Foo = struct { ... };`).
15
+ */
16
+ export function extractZigSymbols(tree: TreeSitterTree, _filePath: string): ExtractorOutput {
17
+ const ctx: ExtractorOutput = {
18
+ definitions: [],
19
+ calls: [],
20
+ imports: [],
21
+ classes: [],
22
+ exports: [],
23
+ typeMap: new Map(),
24
+ };
25
+
26
+ walkZigNode(tree.rootNode, ctx);
27
+ return ctx;
28
+ }
29
+
30
+ function walkZigNode(node: TreeSitterNode, ctx: ExtractorOutput): void {
31
+ switch (node.type) {
32
+ case 'function_declaration':
33
+ handleZigFunction(node, ctx);
34
+ break;
35
+ case 'variable_declaration':
36
+ handleZigVariable(node, ctx);
37
+ break;
38
+ case 'call_expression':
39
+ handleZigCallExpression(node, ctx);
40
+ break;
41
+ case 'builtin_function':
42
+ handleZigBuiltin(node, ctx);
43
+ break;
44
+ case 'test_declaration':
45
+ handleZigTest(node, ctx);
46
+ break;
47
+ }
48
+
49
+ for (let i = 0; i < node.childCount; i++) {
50
+ const child = node.child(i);
51
+ if (child) walkZigNode(child, ctx);
52
+ }
53
+ }
54
+
55
+ function isInsideZigContainer(node: TreeSitterNode): boolean {
56
+ let current = node.parent;
57
+ while (current) {
58
+ if (current.type === 'struct_declaration' || current.type === 'union_declaration') return true;
59
+ current = current.parent;
60
+ }
61
+ return false;
62
+ }
63
+
64
+ function handleZigFunction(node: TreeSitterNode, ctx: ExtractorOutput): void {
65
+ if (isInsideZigContainer(node)) return; // already emitted by extractZigContainerMethods
66
+
67
+ const nameNode = node.childForFieldName('name');
68
+ if (!nameNode) return;
69
+
70
+ const params = extractZigParams(node);
71
+
72
+ ctx.definitions.push({
73
+ name: nameNode.text,
74
+ kind: 'function',
75
+ line: node.startPosition.row + 1,
76
+ endLine: nodeEndLine(node),
77
+ children: params.length > 0 ? params : undefined,
78
+ visibility: isZigPub(node) ? 'public' : 'private',
79
+ });
80
+ }
81
+
82
+ function extractZigParams(funcNode: TreeSitterNode): SubDeclaration[] {
83
+ const params: SubDeclaration[] = [];
84
+ const paramList = funcNode.childForFieldName('parameters');
85
+ if (!paramList) return params;
86
+
87
+ for (let i = 0; i < paramList.childCount; i++) {
88
+ const param = paramList.child(i);
89
+ if (!param || param.type !== 'parameter') continue;
90
+ const nameNode = findChild(param, 'identifier');
91
+ if (nameNode) {
92
+ params.push({ name: nameNode.text, kind: 'parameter', line: param.startPosition.row + 1 });
93
+ }
94
+ }
95
+ return params;
96
+ }
97
+
98
+ function handleZigVariable(node: TreeSitterNode, ctx: ExtractorOutput): void {
99
+ const nameNode = findChild(node, 'identifier');
100
+ if (!nameNode) return;
101
+ const name = nameNode.text;
102
+
103
+ // Check if this is a struct/enum/union definition
104
+ for (let i = 0; i < node.childCount; i++) {
105
+ const child = node.child(i);
106
+ if (!child) continue;
107
+
108
+ if (child.type === 'struct_declaration') {
109
+ const members = extractZigContainerFields(child);
110
+ ctx.definitions.push({
111
+ name,
112
+ kind: 'struct',
113
+ line: node.startPosition.row + 1,
114
+ endLine: nodeEndLine(node),
115
+ children: members.length > 0 ? members : undefined,
116
+ visibility: isZigPub(node) ? 'public' : undefined,
117
+ });
118
+ extractZigContainerMethods(child, name, ctx);
119
+ return;
120
+ }
121
+ if (child.type === 'enum_declaration') {
122
+ ctx.definitions.push({
123
+ name,
124
+ kind: 'enum',
125
+ line: node.startPosition.row + 1,
126
+ endLine: nodeEndLine(node),
127
+ visibility: isZigPub(node) ? 'public' : undefined,
128
+ });
129
+ return;
130
+ }
131
+ if (child.type === 'union_declaration') {
132
+ ctx.definitions.push({
133
+ name,
134
+ kind: 'struct',
135
+ line: node.startPosition.row + 1,
136
+ endLine: nodeEndLine(node),
137
+ visibility: isZigPub(node) ? 'public' : undefined,
138
+ });
139
+ return;
140
+ }
141
+ }
142
+
143
+ // Check for @import
144
+ for (let i = 0; i < node.childCount; i++) {
145
+ const child = node.child(i);
146
+ if (!child) continue;
147
+ if (child.type === 'builtin_function') {
148
+ const builtinId = findChild(child, 'builtin_identifier');
149
+ if (builtinId?.text === '@import') {
150
+ const args = findChild(child, 'arguments');
151
+ if (args) {
152
+ const strArg = findChild(args, 'string_literal') || findChild(args, 'string');
153
+ if (strArg) {
154
+ const source = strArg.text.replace(/^"|"$/g, '');
155
+ ctx.imports.push({
156
+ source,
157
+ names: [name],
158
+ line: node.startPosition.row + 1,
159
+ });
160
+ return;
161
+ }
162
+ }
163
+ }
164
+ }
165
+ }
166
+
167
+ // Regular constant/variable
168
+ const isConst = hasChildText(node, 'const');
169
+ ctx.definitions.push({
170
+ name,
171
+ kind: isConst ? 'constant' : 'variable',
172
+ line: node.startPosition.row + 1,
173
+ endLine: nodeEndLine(node),
174
+ });
175
+ }
176
+
177
+ function extractZigContainerFields(container: TreeSitterNode): SubDeclaration[] {
178
+ const fields: SubDeclaration[] = [];
179
+ for (let i = 0; i < container.childCount; i++) {
180
+ const child = container.child(i);
181
+ if (!child || child.type !== 'container_field') continue;
182
+ const nameNode = child.childForFieldName('name') || findChild(child, 'identifier');
183
+ if (nameNode) {
184
+ fields.push({ name: nameNode.text, kind: 'property', line: child.startPosition.row + 1 });
185
+ }
186
+ }
187
+ return fields;
188
+ }
189
+
190
+ function extractZigContainerMethods(
191
+ container: TreeSitterNode,
192
+ parentName: string,
193
+ ctx: ExtractorOutput,
194
+ ): void {
195
+ for (let i = 0; i < container.childCount; i++) {
196
+ const child = container.child(i);
197
+ if (!child || child.type !== 'function_declaration') continue;
198
+ const nameNode = child.childForFieldName('name');
199
+ if (nameNode) {
200
+ ctx.definitions.push({
201
+ name: `${parentName}.${nameNode.text}`,
202
+ kind: 'method',
203
+ line: child.startPosition.row + 1,
204
+ endLine: nodeEndLine(child),
205
+ visibility: isZigPub(child) ? 'public' : 'private',
206
+ });
207
+ }
208
+ }
209
+ }
210
+
211
+ function handleZigCallExpression(node: TreeSitterNode, ctx: ExtractorOutput): void {
212
+ const funcNode = node.childForFieldName('function');
213
+ if (!funcNode) return;
214
+
215
+ const call: Call = { name: '', line: node.startPosition.row + 1 };
216
+
217
+ if (funcNode.type === 'field_expression' || funcNode.type === 'field_access') {
218
+ const field = funcNode.childForFieldName('field') || funcNode.childForFieldName('member');
219
+ const value = funcNode.childForFieldName('value') || funcNode.child(0);
220
+ if (field) call.name = field.text;
221
+ if (value) call.receiver = value.text;
222
+ } else {
223
+ call.name = funcNode.text;
224
+ }
225
+
226
+ if (call.name) ctx.calls.push(call);
227
+ }
228
+
229
+ function handleZigBuiltin(node: TreeSitterNode, ctx: ExtractorOutput): void {
230
+ const builtinId = findChild(node, 'builtin_identifier');
231
+ if (!builtinId) return;
232
+
233
+ // Treat @import as import (when standalone, not in variable_declaration)
234
+ if (builtinId.text === '@import' && node.parent?.type !== 'variable_declaration') {
235
+ const args = findChild(node, 'arguments');
236
+ if (args) {
237
+ const strArg = findChild(args, 'string_literal') || findChild(args, 'string');
238
+ if (strArg) {
239
+ const source = strArg.text.replace(/^"|"$/g, '');
240
+ ctx.imports.push({
241
+ source,
242
+ names: ['@import'],
243
+ line: node.startPosition.row + 1,
244
+ });
245
+ }
246
+ }
247
+ return;
248
+ }
249
+
250
+ // Other builtins are calls
251
+ ctx.calls.push({ name: builtinId.text, line: node.startPosition.row + 1 });
252
+ }
253
+
254
+ function handleZigTest(node: TreeSitterNode, ctx: ExtractorOutput): void {
255
+ let name = 'test';
256
+ for (let i = 0; i < node.childCount; i++) {
257
+ const child = node.child(i);
258
+ if (!child) continue;
259
+ if (child.type === 'string_literal' || child.type === 'string') {
260
+ // Extract the string content child if available, otherwise strip quotes
261
+ const content = findChild(child, 'string_content');
262
+ name = content ? content.text : child.text.replace(/^"|"$/g, '');
263
+ break;
264
+ }
265
+ if (child.type === 'identifier') {
266
+ name = child.text;
267
+ break;
268
+ }
269
+ }
270
+
271
+ ctx.definitions.push({
272
+ name,
273
+ kind: 'function',
274
+ line: node.startPosition.row + 1,
275
+ endLine: nodeEndLine(node),
276
+ });
277
+ }
278
+
279
+ function isZigPub(node: TreeSitterNode): boolean {
280
+ for (let i = 0; i < node.childCount; i++) {
281
+ const child = node.child(i);
282
+ if (child && child.type === 'pub') return true;
283
+ if (child && child.text === 'pub') return true;
284
+ }
285
+ return false;
286
+ }
287
+
288
+ function hasChildText(node: TreeSitterNode, text: string): boolean {
289
+ for (let i = 0; i < node.childCount; i++) {
290
+ const child = node.child(i);
291
+ if (child && child.text === text) return true;
292
+ }
293
+ return false;
294
+ }
@@ -12,10 +12,9 @@ import type { ASTNodeKind, BetterSqlite3Database, Definition, TreeSitterNode } f
12
12
 
13
13
  // ─── Constants ────────────────────────────────────────────────────────
14
14
 
15
- export const AST_NODE_KINDS: ASTNodeKind[] = ['call', 'new', 'string', 'regex', 'throw', 'await'];
15
+ export const AST_NODE_KINDS: ASTNodeKind[] = ['new', 'string', 'regex', 'throw', 'await'];
16
16
 
17
17
  const KIND_ICONS: Record<string, string> = {
18
- call: '\u0192', // ƒ
19
18
  new: '\u2295', // ⊕
20
19
  string: '"',
21
20
  regex: '/',
@@ -369,7 +369,7 @@ export async function buildCFGData(
369
369
  db: BetterSqlite3Database,
370
370
  fileSymbols: Map<string, FileSymbols>,
371
371
  rootDir: string,
372
- engineOpts?: {
372
+ _engineOpts?: {
373
373
  nativeDb?: { bulkInsertCfg?(entries: Array<Record<string, unknown>>): number };
374
374
  suspendJsDb?: () => void;
375
375
  resumeJsDb?: () => void;
@@ -379,56 +379,11 @@ export async function buildCFGData(
379
379
  // skip WASM parser init, tree parsing, and JS visitor entirely — just persist.
380
380
  const allNative = allCfgNative(fileSymbols);
381
381
 
382
- // ── Native bulk-insert fast path ──────────────────────────────────────
383
- const nativeDb = engineOpts?.nativeDb;
384
- if (allNative && nativeDb?.bulkInsertCfg) {
385
- const entries: Array<Record<string, unknown>> = [];
386
-
387
- for (const [relPath, symbols] of fileSymbols) {
388
- const ext = path.extname(relPath).toLowerCase();
389
- if (!CFG_EXTENSIONS.has(ext)) continue;
390
-
391
- for (const def of symbols.definitions) {
392
- if (def.kind !== 'function' && def.kind !== 'method') continue;
393
- if (!def.line) continue;
394
-
395
- const nodeId = getFunctionNodeId(db, def.name, relPath, def.line);
396
- if (!nodeId) continue;
397
-
398
- deleteCfgForNode(db, nodeId);
399
- if (!def.cfg?.blocks?.length) continue;
400
-
401
- const cfg = def.cfg as unknown as { blocks: CfgBuildBlock[]; edges: CfgBuildEdge[] };
402
- entries.push({
403
- nodeId,
404
- blocks: cfg.blocks.map((b) => ({
405
- index: b.index,
406
- blockType: b.type,
407
- startLine: b.startLine ?? null,
408
- endLine: b.endLine ?? null,
409
- label: b.label ?? null,
410
- })),
411
- edges: cfg.edges.map((e) => ({
412
- sourceIndex: e.sourceIndex,
413
- targetIndex: e.targetIndex,
414
- kind: e.kind,
415
- })),
416
- });
417
- }
418
- }
419
-
420
- if (entries.length > 0) {
421
- let inserted: number;
422
- try {
423
- engineOpts?.suspendJsDb?.();
424
- inserted = nativeDb.bulkInsertCfg(entries);
425
- } finally {
426
- engineOpts?.resumeJsDb?.();
427
- }
428
- info(`CFG (native bulk): ${inserted} blocks across ${entries.length} functions`);
429
- }
430
- return;
431
- }
382
+ // NOTE: nativeDb.bulkInsertCfg is intentionally NOT used here.
383
+ // The CFG path requires delete-before-insert (deleteCfgForNode) which creates
384
+ // a dual-connection WAL conflict when deletes go through JS (better-sqlite3)
385
+ // and inserts go through native (rusqlite). The JS-only persistNativeFileCfg
386
+ // path below handles both on a single connection safely.
432
387
 
433
388
  const extToLang = buildExtToLangMap();
434
389
  let parsers: unknown = null;
@@ -1,3 +1,4 @@
1
+ import { loadNative } from '../../infrastructure/native.js';
1
2
  import type { CodeGraph } from '../model.js';
2
3
 
3
4
  export interface BfsOpts {
@@ -8,6 +9,8 @@ export interface BfsOpts {
8
9
  /**
9
10
  * Breadth-first traversal on a CodeGraph.
10
11
  *
12
+ * Tries the native Rust implementation first, falls back to JS.
13
+ *
11
14
  * @returns nodeId → depth from nearest start node
12
15
  */
13
16
  export function bfs(
@@ -19,6 +22,37 @@ export function bfs(
19
22
  const direction = opts.direction ?? 'forward';
20
23
  const starts = Array.isArray(startIds) ? startIds : [startIds];
21
24
 
25
+ const native = loadNative();
26
+ if (native?.bfsTraversal) {
27
+ const edges = graph.toEdgeArray();
28
+ const nativeMaxDepth = maxDepth === Infinity ? null : maxDepth;
29
+ // Undirected graphs deduplicate edges to one canonical direction in toEdgeArray(),
30
+ // so the Rust side must traverse both directions to preserve symmetry.
31
+ const nativeDirection = !graph.directed ? 'both' : direction;
32
+ const result = native.bfsTraversal(edges, starts, nativeMaxDepth, nativeDirection);
33
+ const depths = new Map<string, number>();
34
+ for (const entry of result) {
35
+ depths.set(entry.node, entry.depth);
36
+ }
37
+ // The Rust side only knows nodes referenced by edges; restore any isolated start nodes.
38
+ for (const startId of starts) {
39
+ if (graph.hasNode(startId) && !depths.has(startId)) {
40
+ depths.set(startId, 0);
41
+ }
42
+ }
43
+ return depths;
44
+ }
45
+
46
+ return bfsJS(graph, starts, maxDepth, direction);
47
+ }
48
+
49
+ /** Pure JS fallback for BFS (used when native addon is unavailable). */
50
+ function bfsJS(
51
+ graph: CodeGraph,
52
+ starts: string[],
53
+ maxDepth: number,
54
+ direction: string,
55
+ ): Map<string, number> {
22
56
  const depths = new Map<string, number>();
23
57
  const queue: string[] = [];
24
58
 
@@ -1,3 +1,4 @@
1
+ import { loadNative } from '../../infrastructure/native.js';
1
2
  import type { CodeGraph } from '../model.js';
2
3
 
3
4
  export interface FanInOut {
@@ -7,8 +8,37 @@ export interface FanInOut {
7
8
 
8
9
  /**
9
10
  * Fan-in / fan-out centrality for all nodes in a CodeGraph.
11
+ *
12
+ * Tries the native Rust implementation first, falls back to JS.
10
13
  */
11
14
  export function fanInOut(graph: CodeGraph): Map<string, FanInOut> {
15
+ const native = loadNative();
16
+ if (native?.fanInOut) {
17
+ let edges = graph.toEdgeArray();
18
+ if (!graph.directed) {
19
+ // Undirected: toEdgeArray() deduplicates to one canonical direction;
20
+ // mirror each edge so the Rust side counts symmetric in/out degrees.
21
+ edges = [...edges, ...edges.map((e) => ({ source: e.target, target: e.source }))];
22
+ }
23
+ const nativeResult = native.fanInOut(edges);
24
+ const result = new Map<string, FanInOut>();
25
+ for (const entry of nativeResult) {
26
+ result.set(entry.node, { fanIn: entry.fanIn, fanOut: entry.fanOut });
27
+ }
28
+ // Ensure isolated nodes (no edges) are included
29
+ for (const id of graph.nodeIds()) {
30
+ if (!result.has(id)) {
31
+ result.set(id, { fanIn: 0, fanOut: 0 });
32
+ }
33
+ }
34
+ return result;
35
+ }
36
+
37
+ return fanInOutJS(graph);
38
+ }
39
+
40
+ /** Pure JS fallback for fan-in/out. */
41
+ function fanInOutJS(graph: CodeGraph): Map<string, FanInOut> {
12
42
  const result = new Map<string, FanInOut>();
13
43
  for (const id of graph.nodeIds()) {
14
44
  result.set(id, {
@@ -1,11 +1,13 @@
1
1
  /**
2
- * Community detection via vendored Leiden algorithm.
2
+ * Community detection via native Rust Louvain or vendored Leiden algorithm.
3
3
  * Maintains backward-compatible API: { assignments: Map<string, number>, modularity: number }
4
4
  *
5
- * Note: Always runs in undirected mode (`directed: false`) regardless of
6
- * the input graph's directedness. For direction-aware community detection,
7
- * use `detectClusters` from `./leiden/index.js` directly.
5
+ * Native path: classic Louvain (Rust, undirected modularity optimization).
6
+ * JS fallback: Leiden algorithm via `detectClusters` (always undirected, `directed: false`).
8
7
  */
8
+
9
+ import { warn } from '../../infrastructure/logger.js';
10
+ import { loadNative } from '../../infrastructure/native.js';
9
11
  import type { CodeGraph } from '../model.js';
10
12
  import type { DetectClustersResult } from './leiden/index.js';
11
13
  import { detectClusters } from './leiden/index.js';
@@ -28,6 +30,31 @@ export function louvainCommunities(graph: CodeGraph, opts: LouvainOptions = {}):
28
30
  }
29
31
 
30
32
  const resolution: number = opts.resolution ?? 1.0;
33
+
34
+ const native = loadNative();
35
+ if (native?.louvainCommunities) {
36
+ // maxLevels, maxLocalPasses, and refinementTheta are Leiden-specific tuning knobs
37
+ // not supported by the Rust Louvain implementation. Warn callers who set them.
38
+ if (opts.maxLevels != null || opts.maxLocalPasses != null || opts.refinementTheta != null) {
39
+ warn(
40
+ 'louvainCommunities: maxLevels/maxLocalPasses/refinementTheta are ignored by the native Rust path',
41
+ );
42
+ }
43
+ const edges = graph.toEdgeArray();
44
+ const nodeIds = graph.nodeIds();
45
+ const result = native.louvainCommunities(edges, nodeIds, resolution, 42);
46
+ const assignments = new Map<string, number>();
47
+ for (const entry of result.assignments) {
48
+ assignments.set(entry.node, entry.community);
49
+ }
50
+ return { assignments, modularity: result.modularity };
51
+ }
52
+
53
+ return louvainJS(graph, opts, resolution);
54
+ }
55
+
56
+ /** JS fallback using the vendored Leiden algorithm. */
57
+ function louvainJS(graph: CodeGraph, opts: LouvainOptions, resolution: number): LouvainResult {
31
58
  const result: DetectClustersResult = detectClusters(graph, {
32
59
  resolution,
33
60
  randomSeed: 42,
@@ -1,8 +1,11 @@
1
+ import { loadNative } from '../../infrastructure/native.js';
1
2
  import type { CodeGraph } from '../model.js';
2
3
 
3
4
  /**
4
5
  * BFS-based shortest path on a CodeGraph.
5
6
  *
7
+ * Tries the native Rust implementation first, falls back to JS.
8
+ *
6
9
  * @returns Path from fromId to toId (inclusive), or null if unreachable
7
10
  */
8
11
  export function shortestPath(graph: CodeGraph, fromId: string, toId: string): string[] | null {
@@ -12,6 +15,23 @@ export function shortestPath(graph: CodeGraph, fromId: string, toId: string): st
12
15
  if (!graph.hasNode(from) || !graph.hasNode(to)) return null;
13
16
  if (from === to) return [from];
14
17
 
18
+ const native = loadNative();
19
+ if (native?.shortestPath) {
20
+ let edges = graph.toEdgeArray();
21
+ if (!graph.directed) {
22
+ // Undirected: toEdgeArray() deduplicates to one canonical direction;
23
+ // mirror each edge so the Rust BFS can traverse in both directions.
24
+ edges = [...edges, ...edges.map((e) => ({ source: e.target, target: e.source }))];
25
+ }
26
+ const result = native.shortestPath(edges, from, to);
27
+ return result.length > 0 ? result : null;
28
+ }
29
+
30
+ return shortestPathJS(graph, from, to);
31
+ }
32
+
33
+ /** Pure JS fallback for shortest path. */
34
+ function shortestPathJS(graph: CodeGraph, from: string, to: string): string[] | null {
15
35
  const parent = new Map<string, string | null>();
16
36
  parent.set(from, null);
17
37
  const queue = [from];
@@ -23,7 +43,6 @@ export function shortestPath(graph: CodeGraph, fromId: string, toId: string): st
23
43
  if (parent.has(neighbor)) continue;
24
44
  parent.set(neighbor, current);
25
45
  if (neighbor === to) {
26
- // Reconstruct path
27
46
  const path: string[] = [];
28
47
  let node: string | null = to;
29
48
  while (node !== null) {