@optave/codegraph 3.8.0 → 3.9.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 (310) hide show
  1. package/README.md +13 -8
  2. package/dist/ast-analysis/engine.d.ts.map +1 -1
  3. package/dist/ast-analysis/engine.js +137 -86
  4. package/dist/ast-analysis/engine.js.map +1 -1
  5. package/dist/ast-analysis/metrics.d.ts +0 -3
  6. package/dist/ast-analysis/metrics.d.ts.map +1 -1
  7. package/dist/ast-analysis/metrics.js +30 -13
  8. package/dist/ast-analysis/metrics.js.map +1 -1
  9. package/dist/ast-analysis/shared.d.ts.map +1 -1
  10. package/dist/ast-analysis/shared.js +24 -19
  11. package/dist/ast-analysis/shared.js.map +1 -1
  12. package/dist/ast-analysis/visitor-utils.d.ts.map +1 -1
  13. package/dist/ast-analysis/visitor-utils.js +55 -39
  14. package/dist/ast-analysis/visitor-utils.js.map +1 -1
  15. package/dist/ast-analysis/visitor.d.ts.map +1 -1
  16. package/dist/ast-analysis/visitor.js +91 -70
  17. package/dist/ast-analysis/visitor.js.map +1 -1
  18. package/dist/ast-analysis/visitors/ast-store-visitor.d.ts.map +1 -1
  19. package/dist/ast-analysis/visitors/ast-store-visitor.js +54 -58
  20. package/dist/ast-analysis/visitors/ast-store-visitor.js.map +1 -1
  21. package/dist/ast-analysis/visitors/complexity-visitor.d.ts.map +1 -1
  22. package/dist/ast-analysis/visitors/complexity-visitor.js +81 -39
  23. package/dist/ast-analysis/visitors/complexity-visitor.js.map +1 -1
  24. package/dist/ast-analysis/visitors/dataflow-visitor.d.ts.map +1 -1
  25. package/dist/ast-analysis/visitors/dataflow-visitor.js +57 -38
  26. package/dist/ast-analysis/visitors/dataflow-visitor.js.map +1 -1
  27. package/dist/cli/commands/branch-compare.d.ts.map +1 -1
  28. package/dist/cli/commands/branch-compare.js +4 -0
  29. package/dist/cli/commands/branch-compare.js.map +1 -1
  30. package/dist/cli/commands/diff-impact.d.ts.map +1 -1
  31. package/dist/cli/commands/diff-impact.js +2 -1
  32. package/dist/cli/commands/diff-impact.js.map +1 -1
  33. package/dist/cli/commands/info.d.ts.map +1 -1
  34. package/dist/cli/commands/info.js +3 -2
  35. package/dist/cli/commands/info.js.map +1 -1
  36. package/dist/cli/commands/watch.d.ts.map +1 -1
  37. package/dist/cli/commands/watch.js +16 -2
  38. package/dist/cli/commands/watch.js.map +1 -1
  39. package/dist/db/connection.d.ts.map +1 -1
  40. package/dist/db/connection.js +29 -26
  41. package/dist/db/connection.js.map +1 -1
  42. package/dist/db/query-builder.d.ts.map +1 -1
  43. package/dist/db/query-builder.js +16 -5
  44. package/dist/db/query-builder.js.map +1 -1
  45. package/dist/db/repository/base.d.ts +16 -0
  46. package/dist/db/repository/base.d.ts.map +1 -1
  47. package/dist/db/repository/base.js +31 -0
  48. package/dist/db/repository/base.js.map +1 -1
  49. package/dist/db/repository/native-repository.d.ts +7 -1
  50. package/dist/db/repository/native-repository.d.ts.map +1 -1
  51. package/dist/db/repository/native-repository.js +100 -1
  52. package/dist/db/repository/native-repository.js.map +1 -1
  53. package/dist/db/repository/nodes.d.ts.map +1 -1
  54. package/dist/db/repository/nodes.js +8 -4
  55. package/dist/db/repository/nodes.js.map +1 -1
  56. package/dist/db/repository/sqlite-repository.d.ts +4 -0
  57. package/dist/db/repository/sqlite-repository.d.ts.map +1 -1
  58. package/dist/db/repository/sqlite-repository.js +51 -0
  59. package/dist/db/repository/sqlite-repository.js.map +1 -1
  60. package/dist/domain/analysis/brief.d.ts.map +1 -1
  61. package/dist/domain/analysis/brief.js +13 -17
  62. package/dist/domain/analysis/brief.js.map +1 -1
  63. package/dist/domain/analysis/context.d.ts.map +1 -1
  64. package/dist/domain/analysis/context.js +14 -11
  65. package/dist/domain/analysis/context.js.map +1 -1
  66. package/dist/domain/analysis/dependencies.d.ts.map +1 -1
  67. package/dist/domain/analysis/dependencies.js +64 -59
  68. package/dist/domain/analysis/dependencies.js.map +1 -1
  69. package/dist/domain/analysis/fn-impact.d.ts +2 -7
  70. package/dist/domain/analysis/fn-impact.d.ts.map +1 -1
  71. package/dist/domain/analysis/fn-impact.js +33 -31
  72. package/dist/domain/analysis/fn-impact.js.map +1 -1
  73. package/dist/domain/analysis/implementations.d.ts.map +1 -1
  74. package/dist/domain/analysis/implementations.js +11 -19
  75. package/dist/domain/analysis/implementations.js.map +1 -1
  76. package/dist/domain/analysis/module-map.d.ts.map +1 -1
  77. package/dist/domain/analysis/module-map.js +55 -76
  78. package/dist/domain/analysis/module-map.js.map +1 -1
  79. package/dist/domain/analysis/query-helpers.d.ts +7 -0
  80. package/dist/domain/analysis/query-helpers.d.ts.map +1 -1
  81. package/dist/domain/analysis/query-helpers.js +15 -1
  82. package/dist/domain/analysis/query-helpers.js.map +1 -1
  83. package/dist/domain/graph/builder/pipeline.d.ts.map +1 -1
  84. package/dist/domain/graph/builder/pipeline.js +352 -107
  85. package/dist/domain/graph/builder/pipeline.js.map +1 -1
  86. package/dist/domain/graph/builder/stages/build-edges.d.ts.map +1 -1
  87. package/dist/domain/graph/builder/stages/build-edges.js +49 -18
  88. package/dist/domain/graph/builder/stages/build-edges.js.map +1 -1
  89. package/dist/domain/graph/builder/stages/detect-changes.js +2 -2
  90. package/dist/domain/graph/builder/stages/detect-changes.js.map +1 -1
  91. package/dist/domain/graph/builder/stages/finalize.js +2 -2
  92. package/dist/domain/graph/builder/stages/finalize.js.map +1 -1
  93. package/dist/domain/graph/builder/stages/insert-nodes.d.ts.map +1 -1
  94. package/dist/domain/graph/builder/stages/insert-nodes.js +32 -21
  95. package/dist/domain/graph/builder/stages/insert-nodes.js.map +1 -1
  96. package/dist/domain/graph/builder/stages/resolve-imports.d.ts.map +1 -1
  97. package/dist/domain/graph/builder/stages/resolve-imports.js +95 -84
  98. package/dist/domain/graph/builder/stages/resolve-imports.js.map +1 -1
  99. package/dist/domain/graph/cycles.d.ts +6 -0
  100. package/dist/domain/graph/cycles.d.ts.map +1 -1
  101. package/dist/domain/graph/cycles.js +114 -22
  102. package/dist/domain/graph/cycles.js.map +1 -1
  103. package/dist/domain/graph/resolve.js +1 -1
  104. package/dist/domain/graph/resolve.js.map +1 -1
  105. package/dist/domain/graph/watcher.d.ts +2 -0
  106. package/dist/domain/graph/watcher.d.ts.map +1 -1
  107. package/dist/domain/graph/watcher.js +170 -75
  108. package/dist/domain/graph/watcher.js.map +1 -1
  109. package/dist/domain/parser.d.ts +3 -4
  110. package/dist/domain/parser.d.ts.map +1 -1
  111. package/dist/domain/parser.js +141 -89
  112. package/dist/domain/parser.js.map +1 -1
  113. package/dist/domain/search/generator.js +1 -1
  114. package/dist/domain/search/generator.js.map +1 -1
  115. package/dist/domain/search/models.d.ts +4 -3
  116. package/dist/domain/search/models.d.ts.map +1 -1
  117. package/dist/domain/search/models.js +23 -8
  118. package/dist/domain/search/models.js.map +1 -1
  119. package/dist/domain/search/search/hybrid.d.ts.map +1 -1
  120. package/dist/domain/search/search/hybrid.js +29 -18
  121. package/dist/domain/search/search/hybrid.js.map +1 -1
  122. package/dist/extractors/go.js +36 -33
  123. package/dist/extractors/go.js.map +1 -1
  124. package/dist/extractors/helpers.d.ts.map +1 -1
  125. package/dist/extractors/helpers.js +40 -29
  126. package/dist/extractors/helpers.js.map +1 -1
  127. package/dist/extractors/java.js +58 -46
  128. package/dist/extractors/java.js.map +1 -1
  129. package/dist/extractors/javascript.js +65 -54
  130. package/dist/extractors/javascript.js.map +1 -1
  131. package/dist/extractors/kotlin.js +84 -78
  132. package/dist/extractors/kotlin.js.map +1 -1
  133. package/dist/extractors/python.js +29 -24
  134. package/dist/extractors/python.js.map +1 -1
  135. package/dist/extractors/rust.js +41 -32
  136. package/dist/extractors/rust.js.map +1 -1
  137. package/dist/extractors/solidity.js +58 -67
  138. package/dist/extractors/solidity.js.map +1 -1
  139. package/dist/extractors/swift.js +83 -81
  140. package/dist/extractors/swift.js.map +1 -1
  141. package/dist/extractors/zig.js +58 -60
  142. package/dist/extractors/zig.js.map +1 -1
  143. package/dist/features/ast.d.ts +16 -14
  144. package/dist/features/ast.d.ts.map +1 -1
  145. package/dist/features/ast.js +83 -81
  146. package/dist/features/ast.js.map +1 -1
  147. package/dist/features/audit.d.ts.map +1 -1
  148. package/dist/features/audit.js +8 -6
  149. package/dist/features/audit.js.map +1 -1
  150. package/dist/features/branch-compare.d.ts.map +1 -1
  151. package/dist/features/branch-compare.js +69 -72
  152. package/dist/features/branch-compare.js.map +1 -1
  153. package/dist/features/communities.d.ts.map +1 -1
  154. package/dist/features/communities.js +19 -7
  155. package/dist/features/communities.js.map +1 -1
  156. package/dist/features/complexity.d.ts.map +1 -1
  157. package/dist/features/complexity.js +120 -125
  158. package/dist/features/complexity.js.map +1 -1
  159. package/dist/features/dataflow.d.ts.map +1 -1
  160. package/dist/features/dataflow.js +136 -137
  161. package/dist/features/dataflow.js.map +1 -1
  162. package/dist/features/flow.d.ts.map +1 -1
  163. package/dist/features/flow.js +84 -79
  164. package/dist/features/flow.js.map +1 -1
  165. package/dist/features/structure-query.d.ts.map +1 -1
  166. package/dist/features/structure-query.js +69 -65
  167. package/dist/features/structure-query.js.map +1 -1
  168. package/dist/graph/algorithms/leiden/optimiser.d.ts.map +1 -1
  169. package/dist/graph/algorithms/leiden/optimiser.js +70 -55
  170. package/dist/graph/algorithms/leiden/optimiser.js.map +1 -1
  171. package/dist/graph/algorithms/leiden/partition.d.ts.map +1 -1
  172. package/dist/graph/algorithms/leiden/partition.js +288 -266
  173. package/dist/graph/algorithms/leiden/partition.js.map +1 -1
  174. package/dist/graph/model.d.ts.map +1 -1
  175. package/dist/graph/model.js +5 -1
  176. package/dist/graph/model.js.map +1 -1
  177. package/dist/infrastructure/config.d.ts.map +1 -1
  178. package/dist/infrastructure/config.js +6 -4
  179. package/dist/infrastructure/config.js.map +1 -1
  180. package/dist/infrastructure/suppress.d.ts +25 -0
  181. package/dist/infrastructure/suppress.d.ts.map +1 -0
  182. package/dist/infrastructure/suppress.js +43 -0
  183. package/dist/infrastructure/suppress.js.map +1 -0
  184. package/dist/mcp/server.d.ts.map +1 -1
  185. package/dist/mcp/server.js +29 -24
  186. package/dist/mcp/server.js.map +1 -1
  187. package/dist/presentation/dataflow.d.ts.map +1 -1
  188. package/dist/presentation/dataflow.js +47 -38
  189. package/dist/presentation/dataflow.js.map +1 -1
  190. package/dist/presentation/diff-impact-mermaid.d.ts.map +1 -1
  191. package/dist/presentation/diff-impact-mermaid.js +60 -51
  192. package/dist/presentation/diff-impact-mermaid.js.map +1 -1
  193. package/dist/presentation/queries-cli/exports.d.ts.map +1 -1
  194. package/dist/presentation/queries-cli/exports.js +20 -14
  195. package/dist/presentation/queries-cli/exports.js.map +1 -1
  196. package/dist/presentation/queries-cli/impact.d.ts.map +1 -1
  197. package/dist/presentation/queries-cli/impact.js +15 -13
  198. package/dist/presentation/queries-cli/impact.js.map +1 -1
  199. package/dist/presentation/queries-cli/inspect.d.ts.map +1 -1
  200. package/dist/presentation/queries-cli/inspect.js +101 -79
  201. package/dist/presentation/queries-cli/inspect.js.map +1 -1
  202. package/dist/presentation/queries-cli/overview.d.ts.map +1 -1
  203. package/dist/presentation/queries-cli/overview.js +25 -16
  204. package/dist/presentation/queries-cli/overview.js.map +1 -1
  205. package/dist/presentation/queries-cli/path.js +26 -20
  206. package/dist/presentation/queries-cli/path.js.map +1 -1
  207. package/dist/presentation/result-formatter.d.ts +10 -0
  208. package/dist/presentation/result-formatter.d.ts.map +1 -1
  209. package/dist/presentation/result-formatter.js +16 -1
  210. package/dist/presentation/result-formatter.js.map +1 -1
  211. package/dist/presentation/viewer.d.ts.map +1 -1
  212. package/dist/presentation/viewer.js +18 -12
  213. package/dist/presentation/viewer.js.map +1 -1
  214. package/dist/shared/errors.d.ts +5 -0
  215. package/dist/shared/errors.d.ts.map +1 -1
  216. package/dist/shared/errors.js +5 -0
  217. package/dist/shared/errors.js.map +1 -1
  218. package/dist/shared/hierarchy.d.ts +8 -2
  219. package/dist/shared/hierarchy.d.ts.map +1 -1
  220. package/dist/shared/hierarchy.js +42 -1
  221. package/dist/shared/hierarchy.js.map +1 -1
  222. package/dist/shared/normalize.d.ts +6 -1
  223. package/dist/shared/normalize.d.ts.map +1 -1
  224. package/dist/shared/normalize.js +20 -12
  225. package/dist/shared/normalize.js.map +1 -1
  226. package/dist/shared/paginate.d.ts +0 -9
  227. package/dist/shared/paginate.d.ts.map +1 -1
  228. package/dist/shared/paginate.js +0 -15
  229. package/dist/shared/paginate.js.map +1 -1
  230. package/dist/types.d.ts +12 -5
  231. package/dist/types.d.ts.map +1 -1
  232. package/grammars/tree-sitter-erlang.wasm +0 -0
  233. package/grammars/tree-sitter-gleam.wasm +0 -0
  234. package/package.json +9 -9
  235. package/src/ast-analysis/engine.ts +176 -104
  236. package/src/ast-analysis/metrics.ts +33 -11
  237. package/src/ast-analysis/shared.ts +33 -24
  238. package/src/ast-analysis/visitor-utils.ts +52 -32
  239. package/src/ast-analysis/visitor.ts +132 -71
  240. package/src/ast-analysis/visitors/ast-store-visitor.ts +53 -50
  241. package/src/ast-analysis/visitors/complexity-visitor.ts +89 -40
  242. package/src/ast-analysis/visitors/dataflow-visitor.ts +87 -43
  243. package/src/cli/commands/branch-compare.ts +4 -0
  244. package/src/cli/commands/diff-impact.ts +2 -1
  245. package/src/cli/commands/info.ts +3 -2
  246. package/src/cli/commands/watch.ts +16 -2
  247. package/src/db/connection.ts +29 -28
  248. package/src/db/query-builder.ts +15 -3
  249. package/src/db/repository/base.ts +34 -0
  250. package/src/db/repository/native-repository.ts +104 -1
  251. package/src/db/repository/nodes.ts +13 -8
  252. package/src/db/repository/sqlite-repository.ts +55 -0
  253. package/src/domain/analysis/brief.ts +15 -25
  254. package/src/domain/analysis/context.ts +17 -10
  255. package/src/domain/analysis/dependencies.ts +77 -81
  256. package/src/domain/analysis/fn-impact.ts +36 -43
  257. package/src/domain/analysis/implementations.ts +11 -17
  258. package/src/domain/analysis/module-map.ts +58 -92
  259. package/src/domain/analysis/query-helpers.ts +18 -1
  260. package/src/domain/graph/builder/pipeline.ts +409 -99
  261. package/src/domain/graph/builder/stages/build-edges.ts +45 -19
  262. package/src/domain/graph/builder/stages/detect-changes.ts +2 -2
  263. package/src/domain/graph/builder/stages/finalize.ts +2 -2
  264. package/src/domain/graph/builder/stages/insert-nodes.ts +59 -34
  265. package/src/domain/graph/builder/stages/resolve-imports.ts +122 -100
  266. package/src/domain/graph/cycles.ts +110 -23
  267. package/src/domain/graph/resolve.ts +1 -1
  268. package/src/domain/graph/watcher.ts +202 -96
  269. package/src/domain/parser.ts +143 -89
  270. package/src/domain/search/generator.ts +1 -1
  271. package/src/domain/search/models.ts +26 -7
  272. package/src/domain/search/search/hybrid.ts +69 -51
  273. package/src/extractors/go.ts +43 -33
  274. package/src/extractors/helpers.ts +37 -23
  275. package/src/extractors/java.ts +66 -47
  276. package/src/extractors/javascript.ts +66 -54
  277. package/src/extractors/kotlin.ts +84 -77
  278. package/src/extractors/python.ts +31 -25
  279. package/src/extractors/rust.ts +37 -29
  280. package/src/extractors/solidity.ts +57 -61
  281. package/src/extractors/swift.ts +81 -80
  282. package/src/extractors/zig.ts +58 -61
  283. package/src/features/ast.ts +130 -110
  284. package/src/features/audit.ts +8 -6
  285. package/src/features/branch-compare.ts +105 -79
  286. package/src/features/communities.ts +25 -10
  287. package/src/features/complexity.ts +171 -134
  288. package/src/features/dataflow.ts +165 -175
  289. package/src/features/flow.ts +129 -92
  290. package/src/features/structure-query.ts +79 -64
  291. package/src/graph/algorithms/leiden/optimiser.ts +99 -55
  292. package/src/graph/algorithms/leiden/partition.ts +359 -294
  293. package/src/graph/model.ts +6 -1
  294. package/src/infrastructure/config.ts +6 -4
  295. package/src/infrastructure/suppress.ts +47 -0
  296. package/src/mcp/server.ts +53 -37
  297. package/src/presentation/dataflow.ts +50 -44
  298. package/src/presentation/diff-impact-mermaid.ts +104 -62
  299. package/src/presentation/queries-cli/exports.ts +21 -13
  300. package/src/presentation/queries-cli/impact.ts +15 -13
  301. package/src/presentation/queries-cli/inspect.ts +100 -81
  302. package/src/presentation/queries-cli/overview.ts +26 -16
  303. package/src/presentation/queries-cli/path.ts +33 -25
  304. package/src/presentation/result-formatter.ts +19 -1
  305. package/src/presentation/viewer.ts +42 -14
  306. package/src/shared/errors.ts +6 -0
  307. package/src/shared/hierarchy.ts +50 -2
  308. package/src/shared/normalize.ts +31 -12
  309. package/src/shared/paginate.ts +0 -17
  310. package/src/types.ts +26 -5
@@ -116,14 +116,13 @@ function isCall(node: TreeSitterNode | null, isCallNode: (t: string) => boolean)
116
116
  return node != null && isCallNode(node.type);
117
117
  }
118
118
 
119
- function handleVarDeclarator(
119
+ /** Resolve the value node from a variable declarator, trying multiple strategies. */
120
+ function resolveValueNode(
120
121
  node: TreeSitterNode,
122
+ nameNode: TreeSitterNode | null,
121
123
  rules: AnyRules,
122
- scopeStack: ScopeEntry[],
123
- assignments: DataflowAssignment[],
124
124
  isCallNode: (t: string) => boolean,
125
- ): void {
126
- let nameNode = node.childForFieldName(rules.varNameField);
125
+ ): TreeSitterNode | null {
127
126
  let valueNode: TreeSitterNode | null = rules.varValueField
128
127
  ? node.childForFieldName(rules.varValueField)
129
128
  : null;
@@ -146,52 +145,97 @@ function handleVarDeclarator(
146
145
  }
147
146
  }
148
147
 
149
- if (rules.expressionListType) {
150
- if (nameNode && nameNode.type === rules.expressionListType)
151
- nameNode = nameNode.namedChildren[0] ?? null;
152
- if (valueNode && valueNode.type === rules.expressionListType)
153
- valueNode = valueNode.namedChildren[0] ?? null;
148
+ return valueNode;
149
+ }
150
+
151
+ /** Unwrap expression-list wrappers from name/value nodes. */
152
+ function unwrapExpressionList(
153
+ nameNode: TreeSitterNode | null,
154
+ valueNode: TreeSitterNode | null,
155
+ rules: AnyRules,
156
+ ): { name: TreeSitterNode | null; value: TreeSitterNode | null } {
157
+ if (!rules.expressionListType) return { name: nameNode, value: valueNode };
158
+ const name =
159
+ nameNode && nameNode.type === rules.expressionListType
160
+ ? (nameNode.namedChildren[0] ?? null)
161
+ : nameNode;
162
+ const value =
163
+ valueNode && valueNode.type === rules.expressionListType
164
+ ? (valueNode.namedChildren[0] ?? null)
165
+ : valueNode;
166
+ return { name, value };
167
+ }
168
+
169
+ /** Record a destructured call assignment (object or array destructuring). */
170
+ function recordDestructuredAssignment(
171
+ nameNode: TreeSitterNode,
172
+ node: TreeSitterNode,
173
+ callee: string,
174
+ scope: ScopeEntry,
175
+ assignments: DataflowAssignment[],
176
+ rules: AnyRules,
177
+ ): void {
178
+ const names = extractParamNames(nameNode, rules);
179
+ for (const n of names) {
180
+ assignments.push({
181
+ varName: n,
182
+ callerFunc: scope.funcName!,
183
+ sourceCallName: callee,
184
+ expression: truncate(node.text),
185
+ line: node.startPosition.row + 1,
186
+ });
187
+ scope.locals.set(n, { type: 'destructured', callee });
154
188
  }
189
+ }
190
+
191
+ /** Record a simple (non-destructured) call assignment. */
192
+ function recordSimpleAssignment(
193
+ nameNode: TreeSitterNode,
194
+ node: TreeSitterNode,
195
+ callee: string,
196
+ scope: ScopeEntry,
197
+ assignments: DataflowAssignment[],
198
+ ): void {
199
+ const varName = nameNode.text;
200
+ assignments.push({
201
+ varName,
202
+ callerFunc: scope.funcName!,
203
+ sourceCallName: callee,
204
+ expression: truncate(node.text),
205
+ line: node.startPosition.row + 1,
206
+ });
207
+ scope.locals.set(varName, { type: 'call_return', callee });
208
+ }
209
+
210
+ function handleVarDeclarator(
211
+ node: TreeSitterNode,
212
+ rules: AnyRules,
213
+ scopeStack: ScopeEntry[],
214
+ assignments: DataflowAssignment[],
215
+ isCallNode: (t: string) => boolean,
216
+ ): void {
217
+ const rawName = node.childForFieldName(rules.varNameField);
218
+ const rawValue = resolveValueNode(node, rawName, rules, isCallNode);
219
+ const { name: nameNode, value: valueNode } = unwrapExpressionList(rawName, rawValue, rules);
155
220
 
156
221
  const scope = currentScope(scopeStack);
157
222
  if (!nameNode || !valueNode || !scope) return;
158
223
 
159
224
  const unwrapped = unwrapAwait(valueNode, rules);
160
225
  const callExpr = isCall(unwrapped, isCallNode) ? unwrapped : null;
226
+ if (!callExpr) return;
161
227
 
162
- if (callExpr) {
163
- const callee = resolveCalleeName(callExpr, rules);
164
- if (callee && scope.funcName) {
165
- if (
166
- (rules.objectDestructType && nameNode.type === rules.objectDestructType) ||
167
- (rules.arrayDestructType && nameNode.type === rules.arrayDestructType)
168
- ) {
169
- const names = extractParamNames(nameNode, rules);
170
- for (const n of names) {
171
- assignments.push({
172
- varName: n,
173
- callerFunc: scope.funcName,
174
- sourceCallName: callee,
175
- expression: truncate(node.text),
176
- line: node.startPosition.row + 1,
177
- });
178
- scope.locals.set(n, { type: 'destructured', callee });
179
- }
180
- } else {
181
- const varName =
182
- nameNode.type === 'identifier' || nameNode.type === rules.paramIdentifier
183
- ? nameNode.text
184
- : nameNode.text;
185
- assignments.push({
186
- varName,
187
- callerFunc: scope.funcName,
188
- sourceCallName: callee,
189
- expression: truncate(node.text),
190
- line: node.startPosition.row + 1,
191
- });
192
- scope.locals.set(varName, { type: 'call_return', callee });
193
- }
194
- }
228
+ const callee = resolveCalleeName(callExpr, rules);
229
+ if (!callee || !scope.funcName) return;
230
+
231
+ const isDestructured =
232
+ (rules.objectDestructType && nameNode.type === rules.objectDestructType) ||
233
+ (rules.arrayDestructType && nameNode.type === rules.arrayDestructType);
234
+
235
+ if (isDestructured) {
236
+ recordDestructuredAssignment(nameNode, node, callee, scope, assignments, rules);
237
+ } else {
238
+ recordSimpleAssignment(nameNode, node, callee, scope, assignments);
195
239
  }
196
240
  }
197
241
 
@@ -5,6 +5,7 @@ export const command: CommandDefinition = {
5
5
  description: 'Compare code structure between two branches/refs',
6
6
  options: [
7
7
  ['--depth <n>', 'Max transitive caller depth', '3'],
8
+ ['-d, --db <path>', 'Path to graph.db'],
8
9
  ['-T, --no-tests', 'Exclude test/spec files'],
9
10
  ['--include-tests', 'Include test/spec files (overrides excludeTests config)'],
10
11
  ['-j, --json', 'Output as JSON'],
@@ -12,12 +13,15 @@ export const command: CommandDefinition = {
12
13
  ],
13
14
  async execute([base, target], opts, ctx) {
14
15
  const { branchCompare } = await import('../../presentation/branch-compare.js');
16
+ const path = await import('node:path');
17
+ const repoRoot = opts.db ? path.resolve(path.dirname(opts.db as string), '..') : undefined;
15
18
  await branchCompare(base!, target!, {
16
19
  engine: ctx.program.opts().engine,
17
20
  depth: parseInt(opts.depth as string, 10),
18
21
  noTests: ctx.resolveNoTests(opts),
19
22
  json: opts.json,
20
23
  format: opts.format,
24
+ repoRoot,
21
25
  });
22
26
  },
23
27
  };
@@ -13,6 +13,7 @@ export const command: CommandDefinition = {
13
13
  ['--ndjson', 'Newline-delimited JSON output'],
14
14
  ['--staged', 'Analyze staged changes instead of unstaged'],
15
15
  ['--depth <n>', 'Max transitive caller depth', '3'],
16
+ ['-j, --json', 'Output as JSON (shorthand for -f json)'],
16
17
  ['-f, --format <format>', 'Output format: text, mermaid, json', 'text'],
17
18
  ['--no-implementations', 'Exclude interface/trait implementors from blast radius'],
18
19
  ],
@@ -21,7 +22,7 @@ export const command: CommandDefinition = {
21
22
  ref,
22
23
  staged: opts.staged,
23
24
  depth: parseInt(opts.depth as string, 10),
24
- format: opts.format,
25
+ format: opts.json ? 'json' : opts.format,
25
26
  includeImplementors: opts.implementations !== false,
26
27
  ...ctx.resolveQueryOpts(opts),
27
28
  });
@@ -3,7 +3,8 @@ import type { CommandDefinition } from '../types.js';
3
3
  export const command: CommandDefinition = {
4
4
  name: 'info',
5
5
  description: 'Show codegraph engine info and diagnostics',
6
- async execute(_args, _opts, ctx) {
6
+ options: [['-d, --db <path>', 'Path to graph.db']],
7
+ async execute(_args, opts, ctx) {
7
8
  const { getNativePackageVersion, isNativeAvailable, loadNative } = await import(
8
9
  '../../infrastructure/native.js'
9
10
  );
@@ -40,7 +41,7 @@ export const command: CommandDefinition = {
40
41
  try {
41
42
  const { findDbPath, getBuildMeta } = await import('../../db/index.js');
42
43
  const Database = (await import('better-sqlite3')).default;
43
- const dbPath = findDbPath();
44
+ const dbPath = findDbPath(opts.db as string | undefined);
44
45
  const fs = await import('node:fs');
45
46
  if (fs.existsSync(dbPath)) {
46
47
  const db = new Database(dbPath, { readonly: true });
@@ -5,9 +5,23 @@ import type { CommandDefinition } from '../types.js';
5
5
  export const command: CommandDefinition = {
6
6
  name: 'watch [dir]',
7
7
  description: 'Watch project for file changes and incrementally update the graph',
8
- async execute([dir], _opts, ctx) {
8
+ options: [
9
+ ['--poll', 'Use stat-based polling (default on Windows to avoid ReFS/Dev Drive crashes)'],
10
+ ['--native', 'Force native OS file watchers instead of polling'],
11
+ ['--poll-interval <ms>', 'Polling interval in milliseconds (default: 2000)'],
12
+ ],
13
+ async execute([dir], opts, ctx) {
9
14
  const root = path.resolve(dir || '.');
10
15
  const engine = ctx.program.opts().engine;
11
- await watchProject(root, { engine });
16
+ if (opts.poll && opts.native) {
17
+ ctx.program.error('--poll and --native are mutually exclusive');
18
+ }
19
+ // Explicit --poll or --native wins; otherwise let watcher auto-detect by platform
20
+ const poll = opts.poll ? true : opts.native ? false : undefined;
21
+ await watchProject(root, {
22
+ engine,
23
+ poll,
24
+ pollInterval: opts.pollInterval ? Number(opts.pollInterval) : undefined,
25
+ });
12
26
  },
13
27
  };
@@ -4,7 +4,7 @@ import path from 'node:path';
4
4
  import { fileURLToPath } from 'node:url';
5
5
  import { debug, warn } from '../infrastructure/logger.js';
6
6
  import { getNative, isNativeAvailable } from '../infrastructure/native.js';
7
- import { DbError } from '../shared/errors.js';
7
+ import { DbError, toErrorMessage } from '../shared/errors.js';
8
8
  import type { BetterSqlite3Database, NativeDatabase } from '../types.js';
9
9
  import { getDatabase } from './better-sqlite3.js';
10
10
  import { Repository } from './repository/base.js';
@@ -20,7 +20,8 @@ function getPackageVersion(): string {
20
20
  const pkgPath = path.join(connDir, '..', '..', 'package.json');
21
21
  _packageVersion = (JSON.parse(fs.readFileSync(pkgPath, 'utf-8')) as { version: string })
22
22
  .version;
23
- } catch {
23
+ } catch (e) {
24
+ debug(`Failed to read package version: ${toErrorMessage(e)}`);
24
25
  _packageVersion = '';
25
26
  }
26
27
  return _packageVersion;
@@ -41,8 +42,8 @@ function warnOnVersionMismatch(getBuildVersion: () => string | undefined | null)
41
42
  `DB was built with codegraph v${buildVersion}, running v${currentVersion}. Consider: codegraph build --no-incremental`,
42
43
  );
43
44
  }
44
- } catch {
45
- // build_meta table may not exist in older DBs — silently ignore
45
+ } catch (e) {
46
+ debug(`Version mismatch check skipped (build_meta may not exist): ${toErrorMessage(e)}`);
46
47
  }
47
48
  }
48
49
 
@@ -78,11 +79,11 @@ export function findRepoRoot(fromDir?: string): string | null {
78
79
  try {
79
80
  root = fs.realpathSync(raw);
80
81
  } catch (e) {
81
- debug(`realpathSync failed for git root "${raw}", using resolve: ${(e as Error).message}`);
82
+ debug(`realpathSync failed for git root "${raw}", using resolve: ${toErrorMessage(e)}`);
82
83
  root = path.resolve(raw);
83
84
  }
84
85
  } catch (e) {
85
- debug(`git rev-parse failed for "${dir}": ${(e as Error).message}`);
86
+ debug(`git rev-parse failed for "${dir}": ${toErrorMessage(e)}`);
86
87
  root = null;
87
88
  }
88
89
  if (!fromDir) {
@@ -103,7 +104,7 @@ function isProcessAlive(pid: number): boolean {
103
104
  process.kill(pid, 0);
104
105
  return true;
105
106
  } catch (e) {
106
- debug(`PID ${pid} not alive: ${(e as NodeJS.ErrnoException).code || (e as Error).message}`);
107
+ debug(`PID ${pid} not alive: ${(e as NodeJS.ErrnoException).code || toErrorMessage(e)}`);
107
108
  return false;
108
109
  }
109
110
  }
@@ -119,12 +120,12 @@ function acquireAdvisoryLock(dbPath: string): void {
119
120
  }
120
121
  }
121
122
  } catch (e) {
122
- debug(`Advisory lock read failed: ${(e as Error).message}`);
123
+ debug(`Advisory lock read failed: ${toErrorMessage(e)}`);
123
124
  }
124
125
  try {
125
126
  fs.writeFileSync(lockPath, String(process.pid), 'utf-8');
126
127
  } catch (e) {
127
- debug(`Advisory lock write failed: ${(e as Error).message}`);
128
+ debug(`Advisory lock write failed: ${toErrorMessage(e)}`);
128
129
  }
129
130
  }
130
131
 
@@ -135,7 +136,7 @@ function releaseAdvisoryLock(lockPath: string): void {
135
136
  fs.unlinkSync(lockPath);
136
137
  }
137
138
  } catch (e) {
138
- debug(`Advisory lock release failed for ${lockPath}: ${(e as Error).message}`);
139
+ debug(`Advisory lock release failed for ${lockPath}: ${toErrorMessage(e)}`);
139
140
  }
140
141
  }
141
142
 
@@ -151,7 +152,7 @@ function isSameDirectory(a: string, b: string): boolean {
151
152
  const sb = fs.statSync(b);
152
153
  return sa.dev === sb.dev && sa.ino === sb.ino;
153
154
  } catch (e) {
154
- debug(`isSameDirectory stat failed: ${(e as Error).message}`);
155
+ debug(`isSameDirectory stat failed: ${toErrorMessage(e)}`);
155
156
  return false;
156
157
  }
157
158
  }
@@ -187,8 +188,8 @@ export function flushDeferredClose(): void {
187
188
  const db = _deferredDbs.pop()!;
188
189
  try {
189
190
  db.close();
190
- } catch {
191
- /* ignore handle may already be closed */
191
+ } catch (e) {
192
+ debug(`Deferred DB close failed (handle may already be closed): ${toErrorMessage(e)}`);
192
193
  }
193
194
  }
194
195
  }
@@ -216,8 +217,8 @@ export function closeDbDeferred(db: LockedDatabase): void {
216
217
  _deferredDbs.splice(idx, 1);
217
218
  try {
218
219
  db.close();
219
- } catch {
220
- /* ignore handle may already be closed by flush */
220
+ } catch (e) {
221
+ debug(`Deferred DB close failed (may already be closed by flush): ${toErrorMessage(e)}`);
221
222
  }
222
223
  }
223
224
  });
@@ -239,8 +240,8 @@ export function closeDbPair(pair: LockedDatabasePair): void {
239
240
  if (pair.nativeDb) {
240
241
  try {
241
242
  pair.nativeDb.close();
242
- } catch {
243
- /* ignore */
243
+ } catch (e) {
244
+ debug(`closeDbPair: native close failed: ${toErrorMessage(e)}`);
244
245
  }
245
246
  }
246
247
  closeDb(pair.db);
@@ -251,8 +252,8 @@ export function closeDbPairDeferred(pair: LockedDatabasePair): void {
251
252
  if (pair.nativeDb) {
252
253
  try {
253
254
  pair.nativeDb.close();
254
- } catch {
255
- /* ignore */
255
+ } catch (e) {
256
+ debug(`closeDbPairDeferred: native close failed: ${toErrorMessage(e)}`);
256
257
  }
257
258
  }
258
259
  closeDbDeferred(pair.db);
@@ -270,7 +271,7 @@ export function findDbPath(customPath?: string): string {
270
271
  try {
271
272
  ceiling = fs.realpathSync(rawCeiling);
272
273
  } catch (e) {
273
- debug(`realpathSync failed for ceiling "${rawCeiling}": ${(e as Error).message}`);
274
+ debug(`realpathSync failed for ceiling "${rawCeiling}": ${toErrorMessage(e)}`);
274
275
  ceiling = rawCeiling;
275
276
  }
276
277
  } else {
@@ -281,7 +282,7 @@ export function findDbPath(customPath?: string): string {
281
282
  try {
282
283
  dir = fs.realpathSync(process.cwd());
283
284
  } catch (e) {
284
- debug(`realpathSync failed for cwd: ${(e as Error).message}`);
285
+ debug(`realpathSync failed for cwd: ${toErrorMessage(e)}`);
285
286
  dir = process.cwd();
286
287
  }
287
288
  while (true) {
@@ -334,9 +335,11 @@ function openRepoNative(customDbPath?: string): { repo: Repository; close(): voi
334
335
  const ndb = native.NativeDatabase.openReadonly(dbPath);
335
336
  try {
336
337
  warnOnVersionMismatch(() => ndb.getBuildMeta('codegraph_version'));
338
+ const repo = new NativeRepository(ndb, dbPath);
337
339
  return {
338
- repo: new NativeRepository(ndb),
340
+ repo,
339
341
  close() {
342
+ repo.closeFallback();
340
343
  ndb.close();
341
344
  },
342
345
  };
@@ -375,9 +378,7 @@ export function openRepo(
375
378
  // Re-throw user-visible errors (e.g. DB not found) — only silently
376
379
  // fall back for native-engine failures (e.g. incompatible native binary).
377
380
  if (e instanceof DbError) throw e;
378
- debug(
379
- `openRepo: native path failed, falling back to better-sqlite3: ${(e as Error).message}`,
380
- );
381
+ debug(`openRepo: native path failed, falling back to better-sqlite3: ${toErrorMessage(e)}`);
381
382
  }
382
383
  }
383
384
 
@@ -411,7 +412,7 @@ export function openReadonlyWithNative(customPath?: string): {
411
412
  const native = getNative();
412
413
  nativeDb = native.NativeDatabase.openReadonly(dbPath);
413
414
  } catch (e) {
414
- debug(`openReadonlyWithNative: native path failed: ${(e as Error).message}`);
415
+ debug(`openReadonlyWithNative: native path failed: ${toErrorMessage(e)}`);
415
416
  }
416
417
  }
417
418
 
@@ -423,8 +424,8 @@ export function openReadonlyWithNative(customPath?: string): {
423
424
  if (nativeDb) {
424
425
  try {
425
426
  nativeDb.close();
426
- } catch {
427
- // already closed or not closeable
427
+ } catch (e) {
428
+ debug(`openReadonlyWithNative: native close failed: ${toErrorMessage(e)}`);
428
429
  }
429
430
  }
430
431
  },
@@ -33,14 +33,26 @@ function validateOrderBy(clause: string): void {
33
33
  }
34
34
  }
35
35
 
36
+ /**
37
+ * Track parenthesis depth change for a character.
38
+ * Returns non-zero only for '(' / ')'; a character that is '(' or ')'
39
+ * can never simultaneously be ',' so the comma check in the caller
40
+ * remains mutually exclusive with the depth update.
41
+ */
42
+ function parenDepthDelta(ch: string): number {
43
+ if (ch === '(') return 1;
44
+ if (ch === ')') return -1;
45
+ return 0;
46
+ }
47
+
36
48
  function splitTopLevelCommas(str: string): string[] {
37
49
  const parts: string[] = [];
38
50
  let depth = 0;
39
51
  let start = 0;
40
52
  for (let i = 0; i < str.length; i++) {
41
- if (str[i] === '(') depth++;
42
- else if (str[i] === ')') depth--;
43
- else if (str[i] === ',' && depth === 0) {
53
+ const ch = str[i]!;
54
+ depth += parenDepthDelta(ch);
55
+ if (ch === ',' && depth === 0) {
44
56
  parts.push(str.slice(start, i).trim());
45
57
  start = i + 1;
46
58
  }
@@ -100,6 +100,20 @@ export class Repository implements IRepository {
100
100
  throw new Error('not implemented');
101
101
  }
102
102
 
103
+ /**
104
+ * Batch version of findCallers — returns callers for multiple node IDs in a
105
+ * single query. Default implementation loops; subclasses override with SQL
106
+ * `IN (...)` for efficiency.
107
+ */
108
+ findCallersBatch(nodeIds: number[]): Map<number, RelatedNodeRow[]> {
109
+ const result = new Map<number, RelatedNodeRow[]>();
110
+ for (const id of nodeIds) {
111
+ const callers = this.findCallers(id);
112
+ if (callers.length > 0) result.set(id, callers);
113
+ }
114
+ return result;
115
+ }
116
+
103
117
  findDistinctCallers(_nodeId: number): RelatedNodeRow[] {
104
118
  throw new Error('not implemented');
105
119
  }
@@ -189,4 +203,24 @@ export class Repository implements IRepository {
189
203
  getComplexityForNode(_nodeId: number): ComplexityMetrics | undefined {
190
204
  throw new Error('not implemented');
191
205
  }
206
+
207
+ // ── Convenience queries ──────────────────────────────────────────────
208
+ /**
209
+ * Look up the stored content hash for a file.
210
+ * Returns null when the file is not in file_hashes or the method is
211
+ * not yet implemented on the concrete repository.
212
+ */
213
+ getFileHash(_file: string): string | null {
214
+ return null;
215
+ }
216
+
217
+ /** Check whether the graph contains any 'implements' edges. */
218
+ hasImplementsEdges(): boolean {
219
+ return false;
220
+ }
221
+
222
+ /** Check whether the co_changes table exists and has data. */
223
+ hasCoChangesTable(): boolean {
224
+ return false;
225
+ }
192
226
  }
@@ -8,9 +8,11 @@
8
8
  * back to the snake_case field names that the Repository interface expects.
9
9
  */
10
10
 
11
+ import Database from 'better-sqlite3';
11
12
  import { ConfigError } from '../../shared/errors.js';
12
13
  import type {
13
14
  AdjacentEdgeRow,
15
+ BetterSqlite3Database,
14
16
  CallableNodeRow,
15
17
  CallEdgeRow,
16
18
  ChildNodeRow,
@@ -159,10 +161,33 @@ function toComplexityMetrics(r: NativeComplexityMetrics): ComplexityMetrics {
159
161
 
160
162
  export class NativeRepository extends Repository {
161
163
  #ndb: NativeDatabase;
164
+ #dbPath?: string;
165
+ #fallbackDb?: BetterSqlite3Database;
162
166
 
163
- constructor(ndb: NativeDatabase) {
167
+ constructor(ndb: NativeDatabase, dbPath?: string) {
164
168
  super();
165
169
  this.#ndb = ndb;
170
+ this.#dbPath = dbPath;
171
+ }
172
+
173
+ /** Lazy better-sqlite3 handle for methods not yet ported to Rust. */
174
+ #getFallbackDb(): BetterSqlite3Database | undefined {
175
+ if (this.#fallbackDb) return this.#fallbackDb;
176
+ if (!this.#dbPath) return undefined;
177
+ try {
178
+ this.#fallbackDb = new Database(this.#dbPath, { readonly: true });
179
+ return this.#fallbackDb;
180
+ } catch {
181
+ return undefined;
182
+ }
183
+ }
184
+
185
+ /** Close the lazy fallback connection if it was opened. */
186
+ closeFallback(): void {
187
+ if (this.#fallbackDb) {
188
+ this.#fallbackDb.close();
189
+ this.#fallbackDb = undefined;
190
+ }
166
191
  }
167
192
 
168
193
  // ── Node lookups ──────────────────────────────────────────────────
@@ -266,6 +291,31 @@ export class NativeRepository extends Repository {
266
291
  return this.#ndb.findCallers(nodeId).map(toRelatedNodeRow);
267
292
  }
268
293
 
294
+ findCallersBatch(nodeIds: number[]): Map<number, RelatedNodeRow[]> {
295
+ if (nodeIds.length === 0) return new Map();
296
+ const placeholders = nodeIds.map(() => '?').join(',');
297
+ const rows = this.#ndb.queryAll(
298
+ `SELECT e.target_id AS queried_id, n.id, n.name, n.kind, n.file, n.line, n.end_line
299
+ FROM edges e JOIN nodes n ON e.source_id = n.id
300
+ WHERE e.target_id IN (${placeholders}) AND e.kind = 'calls'`,
301
+ nodeIds,
302
+ ) as Array<Record<string, unknown>>;
303
+ const result = new Map<number, RelatedNodeRow[]>();
304
+ for (const row of rows) {
305
+ const qid = row.queried_id as number;
306
+ if (!result.has(qid)) result.set(qid, []);
307
+ result.get(qid)!.push({
308
+ id: row.id as number,
309
+ name: row.name as string,
310
+ kind: row.kind as string,
311
+ file: row.file as string,
312
+ line: row.line as number,
313
+ end_line: (row.end_line as number | null) ?? null,
314
+ });
315
+ }
316
+ return result;
317
+ }
318
+
269
319
  findDistinctCallers(nodeId: number): RelatedNodeRow[] {
270
320
  return this.#ndb.findDistinctCallers(nodeId).map(toRelatedNodeRow);
271
321
  }
@@ -358,4 +408,57 @@ export class NativeRepository extends Repository {
358
408
  const r = this.#ndb.getComplexityForNode(nodeId);
359
409
  return r ? toComplexityMetrics(r) : undefined;
360
410
  }
411
+
412
+ // ── Convenience queries ────────────────────────────────────────────
413
+
414
+ getFileHash(file: string): string | null {
415
+ if (typeof this.#ndb.getFileHash === 'function') return this.#ndb.getFileHash(file);
416
+ // Fallback to better-sqlite3 until Rust implements getFileHash
417
+ const db = this.#getFallbackDb();
418
+ if (db) {
419
+ const row = db.prepare('SELECT hash FROM file_hashes WHERE file = ?').get(file) as
420
+ | { hash: string }
421
+ | undefined;
422
+ return row?.hash ?? null;
423
+ }
424
+ return null;
425
+ }
426
+
427
+ #implementsEdgesCache?: boolean;
428
+ hasImplementsEdges(): boolean {
429
+ if (this.#implementsEdgesCache !== undefined) return this.#implementsEdgesCache;
430
+ if (typeof this.#ndb.hasImplementsEdges === 'function') {
431
+ this.#implementsEdgesCache = this.#ndb.hasImplementsEdges();
432
+ return this.#implementsEdgesCache;
433
+ }
434
+ // Fallback to better-sqlite3
435
+ const db = this.#getFallbackDb();
436
+ if (db) {
437
+ this.#implementsEdgesCache = !!db
438
+ .prepare("SELECT 1 FROM edges WHERE kind = 'implements' LIMIT 1")
439
+ .get();
440
+ return this.#implementsEdgesCache;
441
+ }
442
+ return true; // conservative: assume yes when no fallback available
443
+ }
444
+
445
+ #coChangesTableCache?: boolean;
446
+ hasCoChangesTable(): boolean {
447
+ if (this.#coChangesTableCache !== undefined) return this.#coChangesTableCache;
448
+ if (typeof this.#ndb.hasCoChangesTable === 'function') {
449
+ this.#coChangesTableCache = this.#ndb.hasCoChangesTable();
450
+ return this.#coChangesTableCache;
451
+ }
452
+ // Fallback to better-sqlite3
453
+ const db = this.#getFallbackDb();
454
+ if (db) {
455
+ try {
456
+ this.#coChangesTableCache = !!db.prepare('SELECT 1 FROM co_changes LIMIT 1').get();
457
+ } catch {
458
+ this.#coChangesTableCache = false;
459
+ }
460
+ return this.#coChangesTableCache;
461
+ }
462
+ return false;
463
+ }
361
464
  }
@@ -42,14 +42,8 @@ export function findNodesWithFanIn(
42
42
  return q.all(db, nativeDb);
43
43
  }
44
44
 
45
- /**
46
- * Fetch nodes for triage scoring: fan-in + complexity + churn.
47
- */
48
- export function findNodesForTriage(
49
- db: BetterSqlite3Database,
50
- opts: TriageQueryOpts = {},
51
- nativeDb?: NativeDatabase,
52
- ): TriageNodeRow[] {
45
+ /** Validate kind and role options, throwing on invalid values. */
46
+ function validateTriageOpts(opts: TriageQueryOpts): void {
53
47
  if (opts.kind && !(EVERY_SYMBOL_KIND as readonly string[]).includes(opts.kind)) {
54
48
  throw new ConfigError(
55
49
  `Invalid kind: ${opts.kind} (expected one of ${EVERY_SYMBOL_KIND.join(', ')})`,
@@ -58,6 +52,17 @@ export function findNodesForTriage(
58
52
  if (opts.role && !VALID_ROLES.includes(opts.role)) {
59
53
  throw new ConfigError(`Invalid role: ${opts.role} (expected one of ${VALID_ROLES.join(', ')})`);
60
54
  }
55
+ }
56
+
57
+ /**
58
+ * Fetch nodes for triage scoring: fan-in + complexity + churn.
59
+ */
60
+ export function findNodesForTriage(
61
+ db: BetterSqlite3Database,
62
+ opts: TriageQueryOpts = {},
63
+ nativeDb?: NativeDatabase,
64
+ ): TriageNodeRow[] {
65
+ validateTriageOpts(opts);
61
66
 
62
67
  const kindsToUse = opts.kind ? [opts.kind] : ['function', 'method', 'class'];
63
68
  const q = new NodeQuery()