@optave/codegraph 3.9.0 → 3.9.2

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 (196) hide show
  1. package/README.md +12 -13
  2. package/dist/ast-analysis/engine.d.ts.map +1 -1
  3. package/dist/ast-analysis/engine.js +78 -48
  4. package/dist/ast-analysis/engine.js.map +1 -1
  5. package/dist/ast-analysis/visitors/ast-store-visitor.d.ts.map +1 -1
  6. package/dist/ast-analysis/visitors/ast-store-visitor.js +15 -18
  7. package/dist/ast-analysis/visitors/ast-store-visitor.js.map +1 -1
  8. package/dist/cli/commands/batch.d.ts.map +1 -1
  9. package/dist/cli/commands/batch.js +5 -17
  10. package/dist/cli/commands/batch.js.map +1 -1
  11. package/dist/cli/commands/structure.d.ts.map +1 -1
  12. package/dist/cli/commands/structure.js +18 -1
  13. package/dist/cli/commands/structure.js.map +1 -1
  14. package/dist/db/connection.d.ts +3 -0
  15. package/dist/db/connection.d.ts.map +1 -1
  16. package/dist/db/connection.js +24 -6
  17. package/dist/db/connection.js.map +1 -1
  18. package/dist/db/index.d.ts +1 -1
  19. package/dist/db/index.d.ts.map +1 -1
  20. package/dist/db/index.js +1 -1
  21. package/dist/db/index.js.map +1 -1
  22. package/dist/db/repository/base.d.ts +35 -0
  23. package/dist/db/repository/base.d.ts.map +1 -1
  24. package/dist/db/repository/base.js +8 -0
  25. package/dist/db/repository/base.js.map +1 -1
  26. package/dist/db/repository/index.d.ts +1 -0
  27. package/dist/db/repository/index.d.ts.map +1 -1
  28. package/dist/db/repository/index.js.map +1 -1
  29. package/dist/db/repository/native-repository.d.ts +7 -1
  30. package/dist/db/repository/native-repository.d.ts.map +1 -1
  31. package/dist/db/repository/native-repository.js +46 -1
  32. package/dist/db/repository/native-repository.js.map +1 -1
  33. package/dist/domain/analysis/context.d.ts.map +1 -1
  34. package/dist/domain/analysis/context.js +5 -15
  35. package/dist/domain/analysis/context.js.map +1 -1
  36. package/dist/domain/analysis/dependencies.d.ts +6 -33
  37. package/dist/domain/analysis/dependencies.d.ts.map +1 -1
  38. package/dist/domain/analysis/dependencies.js +18 -16
  39. package/dist/domain/analysis/dependencies.js.map +1 -1
  40. package/dist/domain/analysis/fn-impact.js +2 -2
  41. package/dist/domain/analysis/fn-impact.js.map +1 -1
  42. package/dist/domain/analysis/implementations.d.ts.map +1 -1
  43. package/dist/domain/analysis/implementations.js +3 -13
  44. package/dist/domain/analysis/implementations.js.map +1 -1
  45. package/dist/domain/graph/builder/context.d.ts +4 -0
  46. package/dist/domain/graph/builder/context.d.ts.map +1 -1
  47. package/dist/domain/graph/builder/context.js +4 -0
  48. package/dist/domain/graph/builder/context.js.map +1 -1
  49. package/dist/domain/graph/builder/incremental.d.ts.map +1 -1
  50. package/dist/domain/graph/builder/incremental.js +18 -0
  51. package/dist/domain/graph/builder/incremental.js.map +1 -1
  52. package/dist/domain/graph/builder/native-db-proxy.d.ts +24 -0
  53. package/dist/domain/graph/builder/native-db-proxy.d.ts.map +1 -0
  54. package/dist/domain/graph/builder/native-db-proxy.js +87 -0
  55. package/dist/domain/graph/builder/native-db-proxy.js.map +1 -0
  56. package/dist/domain/graph/builder/pipeline.d.ts.map +1 -1
  57. package/dist/domain/graph/builder/pipeline.js +410 -349
  58. package/dist/domain/graph/builder/pipeline.js.map +1 -1
  59. package/dist/domain/graph/builder/stages/build-edges.d.ts.map +1 -1
  60. package/dist/domain/graph/builder/stages/build-edges.js +44 -4
  61. package/dist/domain/graph/builder/stages/build-edges.js.map +1 -1
  62. package/dist/domain/graph/builder/stages/build-structure.js +2 -2
  63. package/dist/domain/graph/builder/stages/build-structure.js.map +1 -1
  64. package/dist/domain/graph/builder/stages/detect-changes.d.ts.map +1 -1
  65. package/dist/domain/graph/builder/stages/detect-changes.js +6 -28
  66. package/dist/domain/graph/builder/stages/detect-changes.js.map +1 -1
  67. package/dist/domain/graph/builder/stages/finalize.js +1 -1
  68. package/dist/domain/graph/builder/stages/finalize.js.map +1 -1
  69. package/dist/domain/graph/builder/stages/insert-nodes.d.ts.map +1 -1
  70. package/dist/domain/graph/builder/stages/insert-nodes.js +16 -12
  71. package/dist/domain/graph/builder/stages/insert-nodes.js.map +1 -1
  72. package/dist/domain/graph/builder/stages/resolve-imports.d.ts.map +1 -1
  73. package/dist/domain/graph/builder/stages/resolve-imports.js +21 -26
  74. package/dist/domain/graph/builder/stages/resolve-imports.js.map +1 -1
  75. package/dist/domain/graph/watcher.d.ts.map +1 -1
  76. package/dist/domain/graph/watcher.js +99 -95
  77. package/dist/domain/graph/watcher.js.map +1 -1
  78. package/dist/domain/parser.d.ts.map +1 -1
  79. package/dist/domain/parser.js +7 -2
  80. package/dist/domain/parser.js.map +1 -1
  81. package/dist/domain/queries.d.ts +1 -1
  82. package/dist/domain/queries.d.ts.map +1 -1
  83. package/dist/domain/queries.js +1 -1
  84. package/dist/domain/queries.js.map +1 -1
  85. package/dist/extractors/go.js +53 -35
  86. package/dist/extractors/go.js.map +1 -1
  87. package/dist/extractors/javascript.js +66 -27
  88. package/dist/extractors/javascript.js.map +1 -1
  89. package/dist/features/audit.d.ts.map +1 -1
  90. package/dist/features/audit.js +3 -2
  91. package/dist/features/audit.js.map +1 -1
  92. package/dist/features/boundaries.d.ts.map +1 -1
  93. package/dist/features/boundaries.js +3 -5
  94. package/dist/features/boundaries.js.map +1 -1
  95. package/dist/features/branch-compare.d.ts.map +1 -1
  96. package/dist/features/branch-compare.js +2 -1
  97. package/dist/features/branch-compare.js.map +1 -1
  98. package/dist/features/complexity.d.ts.map +1 -1
  99. package/dist/features/complexity.js +78 -58
  100. package/dist/features/complexity.js.map +1 -1
  101. package/dist/features/dataflow.d.ts.map +1 -1
  102. package/dist/features/dataflow.js +109 -118
  103. package/dist/features/dataflow.js.map +1 -1
  104. package/dist/features/flow.d.ts.map +1 -1
  105. package/dist/features/flow.js +2 -1
  106. package/dist/features/flow.js.map +1 -1
  107. package/dist/features/manifesto.d.ts.map +1 -1
  108. package/dist/features/manifesto.js +15 -1
  109. package/dist/features/manifesto.js.map +1 -1
  110. package/dist/features/structure.d.ts.map +1 -1
  111. package/dist/features/structure.js +147 -97
  112. package/dist/features/structure.js.map +1 -1
  113. package/dist/graph/algorithms/louvain.d.ts.map +1 -1
  114. package/dist/graph/algorithms/louvain.js +4 -2
  115. package/dist/graph/algorithms/louvain.js.map +1 -1
  116. package/dist/graph/classifiers/roles.d.ts +2 -0
  117. package/dist/graph/classifiers/roles.d.ts.map +1 -1
  118. package/dist/graph/classifiers/roles.js +13 -5
  119. package/dist/graph/classifiers/roles.js.map +1 -1
  120. package/dist/infrastructure/config.d.ts +1 -0
  121. package/dist/infrastructure/config.d.ts.map +1 -1
  122. package/dist/infrastructure/config.js +1 -0
  123. package/dist/infrastructure/config.js.map +1 -1
  124. package/dist/presentation/batch.d.ts.map +1 -1
  125. package/dist/presentation/batch.js +1 -0
  126. package/dist/presentation/batch.js.map +1 -1
  127. package/dist/presentation/communities.d.ts.map +1 -1
  128. package/dist/presentation/communities.js +38 -34
  129. package/dist/presentation/communities.js.map +1 -1
  130. package/dist/presentation/manifesto.d.ts.map +1 -1
  131. package/dist/presentation/manifesto.js +31 -33
  132. package/dist/presentation/manifesto.js.map +1 -1
  133. package/dist/presentation/queries-cli/inspect.d.ts.map +1 -1
  134. package/dist/presentation/queries-cli/inspect.js +47 -46
  135. package/dist/presentation/queries-cli/inspect.js.map +1 -1
  136. package/dist/presentation/structure.d.ts +1 -1
  137. package/dist/presentation/structure.d.ts.map +1 -1
  138. package/dist/presentation/structure.js +1 -1
  139. package/dist/presentation/structure.js.map +1 -1
  140. package/dist/shared/file-utils.d.ts.map +1 -1
  141. package/dist/shared/file-utils.js +94 -72
  142. package/dist/shared/file-utils.js.map +1 -1
  143. package/dist/shared/normalize.d.ts +12 -0
  144. package/dist/shared/normalize.d.ts.map +1 -1
  145. package/dist/shared/normalize.js +4 -0
  146. package/dist/shared/normalize.js.map +1 -1
  147. package/dist/types.d.ts +82 -1
  148. package/dist/types.d.ts.map +1 -1
  149. package/package.json +7 -7
  150. package/src/ast-analysis/engine.ts +99 -55
  151. package/src/ast-analysis/visitors/ast-store-visitor.ts +19 -21
  152. package/src/cli/commands/batch.ts +5 -26
  153. package/src/cli/commands/structure.ts +21 -1
  154. package/src/db/connection.ts +26 -7
  155. package/src/db/index.ts +2 -0
  156. package/src/db/repository/base.ts +43 -0
  157. package/src/db/repository/index.ts +1 -0
  158. package/src/db/repository/native-repository.ts +67 -1
  159. package/src/domain/analysis/context.ts +5 -15
  160. package/src/domain/analysis/dependencies.ts +19 -16
  161. package/src/domain/analysis/fn-impact.ts +2 -2
  162. package/src/domain/analysis/implementations.ts +3 -13
  163. package/src/domain/graph/builder/context.ts +4 -0
  164. package/src/domain/graph/builder/incremental.ts +21 -0
  165. package/src/domain/graph/builder/native-db-proxy.ts +98 -0
  166. package/src/domain/graph/builder/pipeline.ts +514 -416
  167. package/src/domain/graph/builder/stages/build-edges.ts +45 -3
  168. package/src/domain/graph/builder/stages/build-structure.ts +2 -2
  169. package/src/domain/graph/builder/stages/detect-changes.ts +11 -33
  170. package/src/domain/graph/builder/stages/finalize.ts +1 -1
  171. package/src/domain/graph/builder/stages/insert-nodes.ts +17 -14
  172. package/src/domain/graph/builder/stages/resolve-imports.ts +22 -23
  173. package/src/domain/graph/watcher.ts +118 -98
  174. package/src/domain/parser.ts +8 -2
  175. package/src/domain/queries.ts +1 -1
  176. package/src/extractors/go.ts +57 -32
  177. package/src/extractors/javascript.ts +67 -27
  178. package/src/features/audit.ts +3 -2
  179. package/src/features/boundaries.ts +3 -5
  180. package/src/features/branch-compare.ts +2 -3
  181. package/src/features/complexity.ts +94 -58
  182. package/src/features/dataflow.ts +153 -132
  183. package/src/features/flow.ts +2 -1
  184. package/src/features/manifesto.ts +15 -1
  185. package/src/features/structure.ts +167 -95
  186. package/src/graph/algorithms/louvain.ts +5 -2
  187. package/src/graph/classifiers/roles.ts +14 -5
  188. package/src/infrastructure/config.ts +1 -0
  189. package/src/presentation/batch.ts +1 -0
  190. package/src/presentation/communities.ts +44 -39
  191. package/src/presentation/manifesto.ts +35 -38
  192. package/src/presentation/queries-cli/inspect.ts +48 -46
  193. package/src/presentation/structure.ts +2 -2
  194. package/src/shared/file-utils.ts +116 -77
  195. package/src/shared/normalize.ts +10 -0
  196. package/src/types.ts +86 -0
@@ -266,44 +266,69 @@ function handleTypedIdentifiers(
266
266
  }
267
267
 
268
268
  /** Infer type from a single RHS expression in a short var declaration. */
269
- function inferShortVarType(
269
+ /** x := Struct{...} — composite literal (confidence 1.0). */
270
+ function inferCompositeLiteral(
270
271
  varNode: TreeSitterNode,
271
272
  rhs: TreeSitterNode,
272
273
  typeMap: Map<string, { type: string; confidence: number }>,
273
- ): void {
274
- // x := Struct{...} composite literal (confidence 1.0)
275
- if (rhs.type === 'composite_literal') {
276
- const typeNode = rhs.childForFieldName('type');
277
- if (typeNode) {
278
- const typeName = extractGoTypeName(typeNode);
279
- if (typeName) setTypeMapEntry(typeMap, varNode.text, typeName, 1.0);
280
- }
281
- }
282
- // x := &Struct{...} — address-of composite literal (confidence 1.0)
283
- if (rhs.type === 'unary_expression') {
284
- const operand = rhs.childForFieldName('operand');
285
- if (operand && operand.type === 'composite_literal') {
286
- const typeNode = operand.childForFieldName('type');
287
- if (typeNode) {
288
- const typeName = extractGoTypeName(typeNode);
289
- if (typeName) setTypeMapEntry(typeMap, varNode.text, typeName, 1.0);
290
- }
291
- }
292
- }
293
- // x := NewFoo() or x := pkg.NewFoo() — factory function (confidence 0.7)
294
- if (rhs.type === 'call_expression') {
295
- const fn = rhs.childForFieldName('function');
296
- if (fn && fn.type === 'selector_expression') {
297
- const field = fn.childForFieldName('field');
298
- if (field?.text.startsWith('New')) {
299
- const typeName = field.text.slice(3);
300
- if (typeName) setTypeMapEntry(typeMap, varNode.text, typeName, 0.7);
301
- }
302
- } else if (fn && fn.type === 'identifier' && fn.text.startsWith('New')) {
303
- const typeName = fn.text.slice(3);
274
+ ): boolean {
275
+ if (rhs.type !== 'composite_literal') return false;
276
+ const typeNode = rhs.childForFieldName('type');
277
+ if (!typeNode) return false;
278
+ const typeName = extractGoTypeName(typeNode);
279
+ if (typeName) setTypeMapEntry(typeMap, varNode.text, typeName, 1.0);
280
+ return true;
281
+ }
282
+
283
+ /** x := &Struct{...} — address-of composite literal (confidence 1.0). */
284
+ function inferAddressOfComposite(
285
+ varNode: TreeSitterNode,
286
+ rhs: TreeSitterNode,
287
+ typeMap: Map<string, { type: string; confidence: number }>,
288
+ ): boolean {
289
+ if (rhs.type !== 'unary_expression') return false;
290
+ const operand = rhs.childForFieldName('operand');
291
+ if (!operand || operand.type !== 'composite_literal') return false;
292
+ const typeNode = operand.childForFieldName('type');
293
+ if (!typeNode) return false;
294
+ const typeName = extractGoTypeName(typeNode);
295
+ if (typeName) setTypeMapEntry(typeMap, varNode.text, typeName, 1.0);
296
+ return true;
297
+ }
298
+
299
+ /** x := NewFoo() or x := pkg.NewFoo() — factory function (confidence 0.7). */
300
+ function inferFactoryCall(
301
+ varNode: TreeSitterNode,
302
+ rhs: TreeSitterNode,
303
+ typeMap: Map<string, { type: string; confidence: number }>,
304
+ ): boolean {
305
+ if (rhs.type !== 'call_expression') return false;
306
+ const fn = rhs.childForFieldName('function');
307
+ if (!fn) return false;
308
+
309
+ if (fn.type === 'selector_expression') {
310
+ const field = fn.childForFieldName('field');
311
+ if (field?.text.startsWith('New')) {
312
+ const typeName = field.text.slice(3);
304
313
  if (typeName) setTypeMapEntry(typeMap, varNode.text, typeName, 0.7);
314
+ return true;
305
315
  }
316
+ } else if (fn.type === 'identifier' && fn.text.startsWith('New')) {
317
+ const typeName = fn.text.slice(3);
318
+ if (typeName) setTypeMapEntry(typeMap, varNode.text, typeName, 0.7);
319
+ return true;
306
320
  }
321
+ return false;
322
+ }
323
+
324
+ function inferShortVarType(
325
+ varNode: TreeSitterNode,
326
+ rhs: TreeSitterNode,
327
+ typeMap: Map<string, { type: string; confidence: number }>,
328
+ ): void {
329
+ if (inferCompositeLiteral(varNode, rhs, typeMap)) return;
330
+ if (inferAddressOfComposite(varNode, rhs, typeMap)) return;
331
+ inferFactoryCall(varNode, rhs, typeMap);
307
332
  }
308
333
 
309
334
  /** Handle short_var_declaration: x := Struct{}, x := &Struct{}, x := NewFoo(). */
@@ -202,6 +202,48 @@ function handleExportCapture(
202
202
  }
203
203
  }
204
204
 
205
+ function handleInterfaceCapture(
206
+ c: Record<string, TreeSitterNode>,
207
+ definitions: Definition[],
208
+ ): void {
209
+ const ifaceNode = c.iface_node!;
210
+ const ifaceName = c.iface_name!.text;
211
+ definitions.push({
212
+ name: ifaceName,
213
+ kind: 'interface',
214
+ line: ifaceNode.startPosition.row + 1,
215
+ endLine: nodeEndLine(ifaceNode),
216
+ });
217
+ const body =
218
+ ifaceNode.childForFieldName('body') ||
219
+ findChild(ifaceNode, 'interface_body') ||
220
+ findChild(ifaceNode, 'object_type');
221
+ if (body) extractInterfaceMethods(body, ifaceName, definitions);
222
+ }
223
+
224
+ function handleTypeCapture(c: Record<string, TreeSitterNode>, definitions: Definition[]): void {
225
+ const typeNode = c.type_node!;
226
+ definitions.push({
227
+ name: c.type_name!.text,
228
+ kind: 'type',
229
+ line: typeNode.startPosition.row + 1,
230
+ endLine: nodeEndLine(typeNode),
231
+ });
232
+ }
233
+
234
+ function handleImportCapture(c: Record<string, TreeSitterNode>, imports: Import[]): void {
235
+ const impNode = c.imp_node!;
236
+ const isTypeOnly = impNode.text.startsWith('import type');
237
+ const modPath = c.imp_source!.text.replace(/['"]/g, '');
238
+ const names = extractImportNames(impNode);
239
+ imports.push({
240
+ source: modPath,
241
+ names,
242
+ line: impNode.startPosition.row + 1,
243
+ typeOnly: isTypeOnly,
244
+ });
245
+ }
246
+
205
247
  /** Dispatch a single query match to the appropriate handler. */
206
248
  function dispatchQueryMatch(
207
249
  c: Record<string, TreeSitterNode>,
@@ -220,35 +262,11 @@ function dispatchQueryMatch(
220
262
  } else if (c.meth_node) {
221
263
  handleMethodCapture(c, definitions);
222
264
  } else if (c.iface_node) {
223
- const ifaceName = c.iface_name!.text;
224
- definitions.push({
225
- name: ifaceName,
226
- kind: 'interface',
227
- line: c.iface_node.startPosition.row + 1,
228
- endLine: nodeEndLine(c.iface_node),
229
- });
230
- const body =
231
- c.iface_node.childForFieldName('body') ||
232
- findChild(c.iface_node, 'interface_body') ||
233
- findChild(c.iface_node, 'object_type');
234
- if (body) extractInterfaceMethods(body, ifaceName, definitions);
265
+ handleInterfaceCapture(c, definitions);
235
266
  } else if (c.type_node) {
236
- definitions.push({
237
- name: c.type_name!.text,
238
- kind: 'type',
239
- line: c.type_node.startPosition.row + 1,
240
- endLine: nodeEndLine(c.type_node),
241
- });
267
+ handleTypeCapture(c, definitions);
242
268
  } else if (c.imp_node) {
243
- const isTypeOnly = c.imp_node.text.startsWith('import type');
244
- const modPath = c.imp_source!.text.replace(/['"]/g, '');
245
- const names = extractImportNames(c.imp_node);
246
- imports.push({
247
- source: modPath,
248
- names,
249
- line: c.imp_node.startPosition.row + 1,
250
- typeOnly: isTypeOnly,
251
- });
269
+ handleImportCapture(c, imports);
252
270
  } else if (c.exp_node) {
253
271
  handleExportCapture(c, exps, imports);
254
272
  } else if (c.callfn_node) {
@@ -264,6 +282,14 @@ function dispatchQueryMatch(
264
282
  } else if (c.callsub_node) {
265
283
  const callInfo = extractCallInfo(c.callsub_fn!, c.callsub_node);
266
284
  if (callInfo) calls.push(callInfo);
285
+ } else if (c.newfn_node) {
286
+ calls.push({
287
+ name: c.newfn_name!.text,
288
+ line: c.newfn_node.startPosition.row + 1,
289
+ });
290
+ } else if (c.newmem_node) {
291
+ const callInfo = extractCallInfo(c.newmem_fn!, c.newmem_node);
292
+ if (callInfo) calls.push(callInfo);
267
293
  } else if (c.assign_node) {
268
294
  handleCommonJSAssignment(c.assign_left!, c.assign_right!, c.assign_node, imports);
269
295
  }
@@ -502,6 +528,9 @@ function walkJavaScriptNode(node: TreeSitterNode, ctx: ExtractorOutput): void {
502
528
  case 'call_expression':
503
529
  handleCallExpr(node, ctx);
504
530
  break;
531
+ case 'new_expression':
532
+ handleNewExpr(node, ctx);
533
+ break;
505
534
  case 'import_statement':
506
535
  handleImportStmt(node, ctx);
507
536
  break;
@@ -689,6 +718,17 @@ function handleCallExpr(node: TreeSitterNode, ctx: ExtractorOutput): void {
689
718
  }
690
719
  }
691
720
 
721
+ function handleNewExpr(node: TreeSitterNode, ctx: ExtractorOutput): void {
722
+ const ctor = node.childForFieldName('constructor') || node.child(1);
723
+ if (!ctor) return;
724
+ if (ctor.type === 'identifier') {
725
+ ctx.calls.push({ name: ctor.text, line: node.startPosition.row + 1 });
726
+ } else if (ctor.type === 'member_expression') {
727
+ const callInfo = extractCallInfo(ctor, node);
728
+ if (callInfo) ctx.calls.push(callInfo);
729
+ }
730
+ }
731
+
692
732
  /** Handle a dynamic import() call expression and add to imports if static. */
693
733
  function handleDynamicImportCall(node: TreeSitterNode, imports: Import[]): void {
694
734
  const args = node.childForFieldName('arguments') || findChild(node, 'arguments');
@@ -7,6 +7,7 @@ import { loadConfig } from '../infrastructure/config.js';
7
7
  import { debug } from '../infrastructure/logger.js';
8
8
  import { isTestFile } from '../infrastructure/test-filter.js';
9
9
  import { toErrorMessage } from '../shared/errors.js';
10
+ import { toSymbolRef } from '../shared/normalize.js';
10
11
  import type { BetterSqlite3Database, CodegraphConfig } from '../types.js';
11
12
  import { RULE_DEFS } from './manifesto.js';
12
13
 
@@ -317,7 +318,7 @@ function enrichSymbol(
317
318
  WHERE e.source_id = ? AND e.kind = 'calls'`,
318
319
  )
319
320
  .all(nodeId) as SymbolRef[]
320
- ).map((c) => ({ name: c.name, kind: c.kind, file: c.file, line: c.line }));
321
+ ).map(toSymbolRef);
321
322
 
322
323
  callers = (
323
324
  db
@@ -327,7 +328,7 @@ function enrichSymbol(
327
328
  WHERE e.target_id = ? AND e.kind = 'calls'`,
328
329
  )
329
330
  .all(nodeId) as SymbolRef[]
330
- ).map((c) => ({ name: c.name, kind: c.kind, file: c.file, line: c.line }));
331
+ ).map(toSymbolRef);
331
332
  if (noTests) callers = callers.filter((c) => !isTestFile(c.file));
332
333
 
333
334
  const testCallerRows = db
@@ -1,5 +1,5 @@
1
- import { debug } from '../infrastructure/logger.js';
2
1
  import { isTestFile } from '../infrastructure/test-filter.js';
2
+ import { BoundaryError } from '../shared/errors.js';
3
3
  import type { BetterSqlite3Database } from '../types.js';
4
4
 
5
5
  // ─── Glob-to-Regex ───────────────────────────────────────────────────
@@ -269,8 +269,7 @@ export function evaluateBoundaries(
269
269
 
270
270
  const { valid, errors } = validateBoundaryConfig(boundaryConfig);
271
271
  if (!valid) {
272
- debug(`boundary config validation failed: ${errors.join('; ')}`);
273
- return { violations: [], violationCount: 0 };
272
+ throw new BoundaryError(`Invalid boundary configuration: ${errors.join('; ')}`);
274
273
  }
275
274
 
276
275
  const modules = resolveModules(boundaryConfig);
@@ -297,8 +296,7 @@ export function evaluateBoundaries(
297
296
  )
298
297
  .all() as Array<{ source: string; target: string }>;
299
298
  } catch (err) {
300
- debug(`boundary edge query failed: ${(err as Error).message}`);
301
- return { violations: [], violationCount: 0 };
299
+ throw new BoundaryError('Boundary evaluation failed', { cause: err as Error });
302
300
  }
303
301
 
304
302
  if (opts.noTests) {
@@ -9,6 +9,7 @@ import { debug } from '../infrastructure/logger.js';
9
9
  import { getNative, isNativeAvailable } from '../infrastructure/native.js';
10
10
  import { isTestFile } from '../infrastructure/test-filter.js';
11
11
  import { toErrorMessage } from '../shared/errors.js';
12
+ import { toSymbolRef } from '../shared/normalize.js';
12
13
  import type { EngineMode, NativeDatabase } from '../types.js';
13
14
 
14
15
  // ─── Git Helpers ────────────────────────────────────────────────────────
@@ -255,9 +256,7 @@ function loadCallersFromDb(
255
256
  if (!visited.has(c.id) && (!noTests || !isTestFile(c.file))) {
256
257
  visited.add(c.id);
257
258
  nextFrontier.push(c.id);
258
- allCallers.add(
259
- JSON.stringify({ name: c.name, kind: c.kind, file: c.file, line: c.line }),
260
- );
259
+ allCallers.add(JSON.stringify(toSymbolRef(c)));
261
260
  }
262
261
  }
263
262
  }
@@ -535,75 +535,97 @@ function upsertAstComplexity(
535
535
  return 1;
536
536
  }
537
537
 
538
- export async function buildComplexityMetrics(
538
+ /** Collect native bulk-insert rows from precomputed complexity data.
539
+ * Returns the rows array, or null if any definition is missing complexity
540
+ * (signalling that JS fallback is needed). */
541
+ function collectNativeBulkRows(
542
+ db: BetterSqlite3Database,
543
+ fileSymbols: Map<string, FileSymbols>,
544
+ ): Array<Record<string, unknown>> | null {
545
+ const rows: Array<Record<string, unknown>> = [];
546
+
547
+ for (const [relPath, symbols] of fileSymbols) {
548
+ for (const def of symbols.definitions) {
549
+ if (def.kind !== 'function' && def.kind !== 'method') continue;
550
+ if (!def.line) continue;
551
+ // Interface/type property signatures and single-line stubs are extracted
552
+ // as methods but the native engine correctly never assigns complexity.
553
+ // Mirror the leniency in initWasmParsersIfNeeded to avoid bailing out
554
+ // of the native bulk-insert path for every TypeScript codebase (#846).
555
+ if (!def.complexity) {
556
+ if (def.name.includes('.') || !def.endLine || def.endLine <= def.line) continue;
557
+ return null; // genuine function body missing complexity — needs JS fallback
558
+ }
559
+ const nodeId = getFunctionNodeId(db, def.name, relPath, def.line);
560
+ if (!nodeId) continue;
561
+ const ch = def.complexity.halstead;
562
+ const cl = def.complexity.loc;
563
+ rows.push({
564
+ nodeId,
565
+ cognitive: def.complexity.cognitive ?? 0,
566
+ cyclomatic: def.complexity.cyclomatic ?? 0,
567
+ maxNesting: def.complexity.maxNesting ?? 0,
568
+ loc: cl ? cl.loc : 0,
569
+ sloc: cl ? cl.sloc : 0,
570
+ commentLines: cl ? cl.commentLines : 0,
571
+ halsteadN1: ch ? ch.n1 : 0,
572
+ halsteadN2: ch ? ch.n2 : 0,
573
+ halsteadBigN1: ch ? ch.bigN1 : 0,
574
+ halsteadBigN2: ch ? ch.bigN2 : 0,
575
+ halsteadVocabulary: ch ? ch.vocabulary : 0,
576
+ halsteadLength: ch ? ch.length : 0,
577
+ halsteadVolume: ch ? ch.volume : 0,
578
+ halsteadDifficulty: ch ? ch.difficulty : 0,
579
+ halsteadEffort: ch ? ch.effort : 0,
580
+ halsteadBugs: ch ? ch.bugs : 0,
581
+ maintainabilityIndex: def.complexity.maintainabilityIndex ?? 0,
582
+ });
583
+ }
584
+ }
585
+
586
+ return rows;
587
+ }
588
+
589
+ /** Try the native bulk-insert fast path. Returns true if all rows were
590
+ * inserted successfully (caller can return early). */
591
+ function tryNativeBulkInsert(
539
592
  db: BetterSqlite3Database,
540
593
  fileSymbols: Map<string, FileSymbols>,
541
- rootDir: string,
542
594
  engineOpts?: {
543
595
  nativeDb?: { bulkInsertComplexity?(rows: Array<Record<string, unknown>>): number };
544
596
  suspendJsDb?: () => void;
545
597
  resumeJsDb?: () => void;
546
598
  },
547
- ): Promise<void> {
548
- // ── Native bulk-insert fast path ──────────────────────────────────────
599
+ ): boolean {
549
600
  const nativeDb = engineOpts?.nativeDb;
550
- if (nativeDb?.bulkInsertComplexity) {
551
- const rows: Array<Record<string, unknown>> = [];
552
- let needsJsFallback = false;
553
-
554
- for (const [relPath, symbols] of fileSymbols) {
555
- for (const def of symbols.definitions) {
556
- if (def.kind !== 'function' && def.kind !== 'method') continue;
557
- if (!def.line) continue;
558
- if (!def.complexity) {
559
- needsJsFallback = true;
560
- break;
561
- }
562
- const nodeId = getFunctionNodeId(db, def.name, relPath, def.line);
563
- if (!nodeId) continue;
564
- const ch = def.complexity.halstead;
565
- const cl = def.complexity.loc;
566
- rows.push({
567
- nodeId,
568
- cognitive: def.complexity.cognitive ?? 0,
569
- cyclomatic: def.complexity.cyclomatic ?? 0,
570
- maxNesting: def.complexity.maxNesting ?? 0,
571
- loc: cl ? cl.loc : 0,
572
- sloc: cl ? cl.sloc : 0,
573
- commentLines: cl ? cl.commentLines : 0,
574
- halsteadN1: ch ? ch.n1 : 0,
575
- halsteadN2: ch ? ch.n2 : 0,
576
- halsteadBigN1: ch ? ch.bigN1 : 0,
577
- halsteadBigN2: ch ? ch.bigN2 : 0,
578
- halsteadVocabulary: ch ? ch.vocabulary : 0,
579
- halsteadLength: ch ? ch.length : 0,
580
- halsteadVolume: ch ? ch.volume : 0,
581
- halsteadDifficulty: ch ? ch.difficulty : 0,
582
- halsteadEffort: ch ? ch.effort : 0,
583
- halsteadBugs: ch ? ch.bugs : 0,
584
- maintainabilityIndex: def.complexity.maintainabilityIndex ?? 0,
585
- });
586
- }
587
- if (needsJsFallback) break;
588
- }
601
+ if (!nativeDb?.bulkInsertComplexity) return false;
602
+
603
+ const rows = collectNativeBulkRows(db, fileSymbols);
604
+ if (rows === null) return false; // missing complexity — needs JS fallback
605
+ if (rows.length === 0) return true; // nothing to insert — native path done
606
+
607
+ let inserted: number;
608
+ try {
609
+ engineOpts?.suspendJsDb?.();
610
+ inserted = nativeDb.bulkInsertComplexity(rows);
611
+ } finally {
612
+ engineOpts?.resumeJsDb?.();
613
+ }
589
614
 
590
- if (!needsJsFallback && rows.length > 0) {
591
- let inserted: number;
592
- try {
593
- engineOpts?.suspendJsDb?.();
594
- inserted = nativeDb.bulkInsertComplexity(rows);
595
- } finally {
596
- engineOpts?.resumeJsDb?.();
597
- }
598
- if (inserted === rows.length) {
599
- info(`Complexity (native bulk): ${inserted} functions analyzed`);
600
- return;
601
- }
602
- debug(`Native bulkInsertComplexity partial: ${inserted}/${rows.length} — falling back to JS`);
603
- }
615
+ if (inserted === rows.length) {
616
+ info(`Complexity (native bulk): ${inserted} functions analyzed`);
617
+ return true;
604
618
  }
619
+ debug(`Native bulkInsertComplexity partial: ${inserted}/${rows.length} — falling back to JS`);
620
+ return false;
621
+ }
605
622
 
606
- // ── JS fallback path ─────────────────────────────────────────────────
623
+ /** JS/WASM fallback: parse files and compute metrics via AST traversal. */
624
+ async function computeJsFallbackMetrics(
625
+ db: BetterSqlite3Database,
626
+ fileSymbols: Map<string, FileSymbols>,
627
+ rootDir: string,
628
+ ): Promise<void> {
607
629
  const { parsers, extToLang } = await initWasmParsersIfNeeded(fileSymbols);
608
630
  const { getParser } = await import('../domain/parser.js');
609
631
 
@@ -649,6 +671,20 @@ export async function buildComplexityMetrics(
649
671
  }
650
672
  }
651
673
 
674
+ export async function buildComplexityMetrics(
675
+ db: BetterSqlite3Database,
676
+ fileSymbols: Map<string, FileSymbols>,
677
+ rootDir: string,
678
+ engineOpts?: {
679
+ nativeDb?: { bulkInsertComplexity?(rows: Array<Record<string, unknown>>): number };
680
+ suspendJsDb?: () => void;
681
+ resumeJsDb?: () => void;
682
+ },
683
+ ): Promise<void> {
684
+ if (tryNativeBulkInsert(db, fileSymbols, engineOpts)) return;
685
+ await computeJsFallbackMetrics(db, fileSymbols, rootDir);
686
+ }
687
+
652
688
  // ─── Query-Time Functions (re-exported from complexity-query.ts) ──────────
653
689
  // Split to separate query-time concerns (DB reads, filtering, pagination)
654
690
  // from compute-time concerns (AST traversal, metric algorithms).