@optave/codegraph 3.11.0 → 3.11.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 (230) hide show
  1. package/README.md +38 -31
  2. package/dist/ast-analysis/engine.d.ts.map +1 -1
  3. package/dist/ast-analysis/engine.js +91 -60
  4. package/dist/ast-analysis/engine.js.map +1 -1
  5. package/dist/ast-analysis/visitor-utils.d.ts +3 -0
  6. package/dist/ast-analysis/visitor-utils.d.ts.map +1 -1
  7. package/dist/ast-analysis/visitor-utils.js +83 -49
  8. package/dist/ast-analysis/visitor-utils.js.map +1 -1
  9. package/dist/ast-analysis/visitors/ast-store-visitor.d.ts.map +1 -1
  10. package/dist/ast-analysis/visitors/ast-store-visitor.js +78 -62
  11. package/dist/ast-analysis/visitors/ast-store-visitor.js.map +1 -1
  12. package/dist/ast-analysis/visitors/dataflow-visitor.d.ts.map +1 -1
  13. package/dist/ast-analysis/visitors/dataflow-visitor.js +61 -42
  14. package/dist/ast-analysis/visitors/dataflow-visitor.js.map +1 -1
  15. package/dist/cli/commands/embed.d.ts.map +1 -1
  16. package/dist/cli/commands/embed.js +49 -4
  17. package/dist/cli/commands/embed.js.map +1 -1
  18. package/dist/domain/analysis/dependencies.d.ts.map +1 -1
  19. package/dist/domain/analysis/dependencies.js +106 -80
  20. package/dist/domain/analysis/dependencies.js.map +1 -1
  21. package/dist/domain/analysis/fn-impact.d.ts.map +1 -1
  22. package/dist/domain/analysis/fn-impact.js +77 -52
  23. package/dist/domain/analysis/fn-impact.js.map +1 -1
  24. package/dist/domain/analysis/module-map.d.ts.map +1 -1
  25. package/dist/domain/analysis/module-map.js +132 -121
  26. package/dist/domain/analysis/module-map.js.map +1 -1
  27. package/dist/domain/graph/builder/call-resolver.d.ts +71 -0
  28. package/dist/domain/graph/builder/call-resolver.d.ts.map +1 -0
  29. package/dist/domain/graph/builder/call-resolver.js +130 -0
  30. package/dist/domain/graph/builder/call-resolver.js.map +1 -0
  31. package/dist/domain/graph/builder/helpers.d.ts +4 -4
  32. package/dist/domain/graph/builder/helpers.d.ts.map +1 -1
  33. package/dist/domain/graph/builder/helpers.js +47 -33
  34. package/dist/domain/graph/builder/helpers.js.map +1 -1
  35. package/dist/domain/graph/builder/incremental.d.ts +6 -0
  36. package/dist/domain/graph/builder/incremental.d.ts.map +1 -1
  37. package/dist/domain/graph/builder/incremental.js +214 -127
  38. package/dist/domain/graph/builder/incremental.js.map +1 -1
  39. package/dist/domain/graph/builder/pipeline.d.ts +1 -44
  40. package/dist/domain/graph/builder/pipeline.d.ts.map +1 -1
  41. package/dist/domain/graph/builder/pipeline.js +10 -766
  42. package/dist/domain/graph/builder/pipeline.js.map +1 -1
  43. package/dist/domain/graph/builder/stages/build-edges.d.ts.map +1 -1
  44. package/dist/domain/graph/builder/stages/build-edges.js +151 -192
  45. package/dist/domain/graph/builder/stages/build-edges.js.map +1 -1
  46. package/dist/domain/graph/builder/stages/build-structure.d.ts.map +1 -1
  47. package/dist/domain/graph/builder/stages/build-structure.js +82 -65
  48. package/dist/domain/graph/builder/stages/build-structure.js.map +1 -1
  49. package/dist/domain/graph/builder/stages/detect-changes.d.ts.map +1 -1
  50. package/dist/domain/graph/builder/stages/detect-changes.js +84 -56
  51. package/dist/domain/graph/builder/stages/detect-changes.js.map +1 -1
  52. package/dist/domain/graph/builder/stages/finalize.d.ts.map +1 -1
  53. package/dist/domain/graph/builder/stages/finalize.js +60 -51
  54. package/dist/domain/graph/builder/stages/finalize.js.map +1 -1
  55. package/dist/domain/graph/builder/stages/insert-nodes.d.ts +8 -6
  56. package/dist/domain/graph/builder/stages/insert-nodes.d.ts.map +1 -1
  57. package/dist/domain/graph/builder/stages/insert-nodes.js +107 -122
  58. package/dist/domain/graph/builder/stages/insert-nodes.js.map +1 -1
  59. package/dist/domain/graph/builder/stages/native-db-lifecycle.d.ts +14 -0
  60. package/dist/domain/graph/builder/stages/native-db-lifecycle.d.ts.map +1 -0
  61. package/dist/domain/graph/builder/stages/native-db-lifecycle.js +77 -0
  62. package/dist/domain/graph/builder/stages/native-db-lifecycle.js.map +1 -0
  63. package/dist/domain/graph/builder/stages/native-orchestrator.d.ts +62 -0
  64. package/dist/domain/graph/builder/stages/native-orchestrator.d.ts.map +1 -0
  65. package/dist/domain/graph/builder/stages/native-orchestrator.js +747 -0
  66. package/dist/domain/graph/builder/stages/native-orchestrator.js.map +1 -0
  67. package/dist/domain/graph/cycles.d.ts +6 -4
  68. package/dist/domain/graph/cycles.d.ts.map +1 -1
  69. package/dist/domain/graph/cycles.js +50 -55
  70. package/dist/domain/graph/cycles.js.map +1 -1
  71. package/dist/domain/graph/journal.d.ts.map +1 -1
  72. package/dist/domain/graph/journal.js +89 -70
  73. package/dist/domain/graph/journal.js.map +1 -1
  74. package/dist/domain/graph/watcher.d.ts.map +1 -1
  75. package/dist/domain/graph/watcher.js +10 -4
  76. package/dist/domain/graph/watcher.js.map +1 -1
  77. package/dist/domain/parser.d.ts +12 -23
  78. package/dist/domain/parser.d.ts.map +1 -1
  79. package/dist/domain/parser.js +126 -79
  80. package/dist/domain/parser.js.map +1 -1
  81. package/dist/domain/search/generator.d.ts +3 -1
  82. package/dist/domain/search/generator.d.ts.map +1 -1
  83. package/dist/domain/search/generator.js +68 -45
  84. package/dist/domain/search/generator.js.map +1 -1
  85. package/dist/domain/search/models.d.ts +2 -0
  86. package/dist/domain/search/models.d.ts.map +1 -1
  87. package/dist/domain/search/models.js +37 -3
  88. package/dist/domain/search/models.js.map +1 -1
  89. package/dist/domain/search/search/hybrid.d.ts.map +1 -1
  90. package/dist/domain/search/search/hybrid.js +49 -40
  91. package/dist/domain/search/search/hybrid.js.map +1 -1
  92. package/dist/domain/search/search/semantic.d.ts.map +1 -1
  93. package/dist/domain/search/search/semantic.js +69 -49
  94. package/dist/domain/search/search/semantic.js.map +1 -1
  95. package/dist/domain/wasm-worker-entry.js +201 -136
  96. package/dist/domain/wasm-worker-entry.js.map +1 -1
  97. package/dist/extractors/elixir.js +95 -71
  98. package/dist/extractors/elixir.js.map +1 -1
  99. package/dist/extractors/gleam.d.ts.map +1 -1
  100. package/dist/extractors/gleam.js +23 -31
  101. package/dist/extractors/gleam.js.map +1 -1
  102. package/dist/extractors/helpers.d.ts +79 -1
  103. package/dist/extractors/helpers.d.ts.map +1 -1
  104. package/dist/extractors/helpers.js +137 -0
  105. package/dist/extractors/helpers.js.map +1 -1
  106. package/dist/extractors/java.d.ts.map +1 -1
  107. package/dist/extractors/java.js +37 -49
  108. package/dist/extractors/java.js.map +1 -1
  109. package/dist/extractors/javascript.d.ts.map +1 -1
  110. package/dist/extractors/javascript.js +44 -44
  111. package/dist/extractors/javascript.js.map +1 -1
  112. package/dist/extractors/julia.js +27 -34
  113. package/dist/extractors/julia.js.map +1 -1
  114. package/dist/extractors/r.d.ts.map +1 -1
  115. package/dist/extractors/r.js +33 -58
  116. package/dist/extractors/r.js.map +1 -1
  117. package/dist/extractors/solidity.d.ts.map +1 -1
  118. package/dist/extractors/solidity.js +38 -61
  119. package/dist/extractors/solidity.js.map +1 -1
  120. package/dist/features/boundaries.d.ts.map +1 -1
  121. package/dist/features/boundaries.js +49 -39
  122. package/dist/features/boundaries.js.map +1 -1
  123. package/dist/features/cfg.d.ts.map +1 -1
  124. package/dist/features/cfg.js +90 -63
  125. package/dist/features/cfg.js.map +1 -1
  126. package/dist/features/check.d.ts.map +1 -1
  127. package/dist/features/check.js +43 -34
  128. package/dist/features/check.js.map +1 -1
  129. package/dist/features/cochange.d.ts.map +1 -1
  130. package/dist/features/cochange.js +68 -56
  131. package/dist/features/cochange.js.map +1 -1
  132. package/dist/features/complexity.d.ts.map +1 -1
  133. package/dist/features/complexity.js +105 -75
  134. package/dist/features/complexity.js.map +1 -1
  135. package/dist/features/dataflow.d.ts.map +1 -1
  136. package/dist/features/dataflow.js +37 -29
  137. package/dist/features/dataflow.js.map +1 -1
  138. package/dist/features/flow.d.ts.map +1 -1
  139. package/dist/features/flow.js +31 -22
  140. package/dist/features/flow.js.map +1 -1
  141. package/dist/features/graph-enrichment.d.ts.map +1 -1
  142. package/dist/features/graph-enrichment.js +77 -70
  143. package/dist/features/graph-enrichment.js.map +1 -1
  144. package/dist/features/owners.d.ts +17 -26
  145. package/dist/features/owners.d.ts.map +1 -1
  146. package/dist/features/owners.js +120 -109
  147. package/dist/features/owners.js.map +1 -1
  148. package/dist/features/sequence.d.ts.map +1 -1
  149. package/dist/features/sequence.js +59 -54
  150. package/dist/features/sequence.js.map +1 -1
  151. package/dist/features/structure-query.d.ts.map +1 -1
  152. package/dist/features/structure-query.js +60 -60
  153. package/dist/features/structure-query.js.map +1 -1
  154. package/dist/features/structure.d.ts.map +1 -1
  155. package/dist/features/structure.js +149 -52
  156. package/dist/features/structure.js.map +1 -1
  157. package/dist/graph/algorithms/leiden/optimiser.d.ts.map +1 -1
  158. package/dist/graph/algorithms/leiden/optimiser.js +100 -69
  159. package/dist/graph/algorithms/leiden/optimiser.js.map +1 -1
  160. package/dist/graph/classifiers/roles.d.ts.map +1 -1
  161. package/dist/graph/classifiers/roles.js +63 -59
  162. package/dist/graph/classifiers/roles.js.map +1 -1
  163. package/dist/infrastructure/config.d.ts +1 -1
  164. package/dist/infrastructure/config.d.ts.map +1 -1
  165. package/dist/infrastructure/config.js +1 -1
  166. package/dist/infrastructure/config.js.map +1 -1
  167. package/dist/presentation/cfg.d.ts.map +1 -1
  168. package/dist/presentation/cfg.js +44 -29
  169. package/dist/presentation/cfg.js.map +1 -1
  170. package/dist/presentation/flow.d.ts.map +1 -1
  171. package/dist/presentation/flow.js +58 -38
  172. package/dist/presentation/flow.js.map +1 -1
  173. package/dist/types.d.ts +1 -1
  174. package/dist/types.d.ts.map +1 -1
  175. package/grammars/tree-sitter-erlang.wasm +0 -0
  176. package/package.json +9 -9
  177. package/src/ast-analysis/engine.ts +145 -61
  178. package/src/ast-analysis/visitor-utils.ts +86 -46
  179. package/src/ast-analysis/visitors/ast-store-visitor.ts +104 -69
  180. package/src/ast-analysis/visitors/dataflow-visitor.ts +86 -47
  181. package/src/cli/commands/embed.ts +54 -4
  182. package/src/domain/analysis/dependencies.ts +166 -85
  183. package/src/domain/analysis/fn-impact.ts +120 -50
  184. package/src/domain/analysis/module-map.ts +175 -140
  185. package/src/domain/graph/builder/call-resolver.ts +181 -0
  186. package/src/domain/graph/builder/helpers.ts +85 -76
  187. package/src/domain/graph/builder/incremental.ts +321 -152
  188. package/src/domain/graph/builder/pipeline.ts +19 -957
  189. package/src/domain/graph/builder/stages/build-edges.ts +229 -275
  190. package/src/domain/graph/builder/stages/build-structure.ts +115 -82
  191. package/src/domain/graph/builder/stages/detect-changes.ts +107 -64
  192. package/src/domain/graph/builder/stages/finalize.ts +72 -70
  193. package/src/domain/graph/builder/stages/insert-nodes.ts +154 -120
  194. package/src/domain/graph/builder/stages/native-db-lifecycle.ts +74 -0
  195. package/src/domain/graph/builder/stages/native-orchestrator.ts +942 -0
  196. package/src/domain/graph/cycles.ts +51 -49
  197. package/src/domain/graph/journal.ts +84 -69
  198. package/src/domain/graph/watcher.ts +12 -4
  199. package/src/domain/parser.ts +143 -66
  200. package/src/domain/search/generator.ts +132 -74
  201. package/src/domain/search/models.ts +39 -3
  202. package/src/domain/search/search/hybrid.ts +53 -42
  203. package/src/domain/search/search/semantic.ts +105 -65
  204. package/src/domain/wasm-worker-entry.ts +235 -152
  205. package/src/extractors/elixir.ts +91 -64
  206. package/src/extractors/gleam.ts +33 -37
  207. package/src/extractors/helpers.ts +205 -1
  208. package/src/extractors/java.ts +42 -45
  209. package/src/extractors/javascript.ts +44 -43
  210. package/src/extractors/julia.ts +28 -35
  211. package/src/extractors/r.ts +38 -56
  212. package/src/extractors/solidity.ts +43 -71
  213. package/src/features/boundaries.ts +64 -46
  214. package/src/features/cfg.ts +145 -74
  215. package/src/features/check.ts +60 -43
  216. package/src/features/cochange.ts +95 -72
  217. package/src/features/complexity.ts +134 -79
  218. package/src/features/dataflow.ts +57 -34
  219. package/src/features/flow.ts +48 -24
  220. package/src/features/graph-enrichment.ts +105 -70
  221. package/src/features/owners.ts +186 -146
  222. package/src/features/sequence.ts +99 -69
  223. package/src/features/structure-query.ts +94 -79
  224. package/src/features/structure.ts +199 -79
  225. package/src/graph/algorithms/leiden/optimiser.ts +142 -87
  226. package/src/graph/classifiers/roles.ts +64 -54
  227. package/src/infrastructure/config.ts +1 -1
  228. package/src/presentation/cfg.ts +48 -32
  229. package/src/presentation/flow.ts +100 -52
  230. package/src/types.ts +1 -1
@@ -31,44 +31,36 @@ const COMPLEXITY_EXTENSIONS = buildExtensionSet(COMPLEXITY_RULES);
31
31
 
32
32
  // ─── Halstead Metrics Computation ─────────────────────────────────────────
33
33
 
34
- export function computeHalsteadMetrics(
35
- functionNode: TreeSitterNode,
36
- language: string,
37
- ): HalsteadDerivedMetrics | null {
38
- const rules = HALSTEAD_RULES.get(language) as HalsteadRules | undefined;
39
- if (!rules) return null;
40
-
41
- const operators = new Map<string, number>(); // type -> count
42
- const operands = new Map<string, number>(); // text -> count
43
-
44
- function walk(node: TreeSitterNode | null): void {
45
- if (!node) return;
46
-
47
- // Skip type annotation subtrees
48
- if (rules?.skipTypes.has(node.type)) return;
34
+ /** Classify a tree-sitter node as a Halstead operator or operand,
35
+ * updating the running counts. Pure helper extracted from computeHalsteadMetrics
36
+ * to keep the dispatcher thin. */
37
+ function classifyHalsteadToken(
38
+ node: TreeSitterNode,
39
+ rules: HalsteadRules,
40
+ operators: Map<string, number>,
41
+ operands: Map<string, number>,
42
+ ): void {
43
+ // Compound operators (non-leaf): count the node type as an operator
44
+ if (rules.compoundOperators.has(node.type)) {
45
+ operators.set(node.type, (operators.get(node.type) || 0) + 1);
46
+ }
49
47
 
50
- // Compound operators (non-leaf): count the node type as an operator
51
- if (rules?.compoundOperators.has(node.type)) {
48
+ // Leaf nodes: classify as operator or operand
49
+ if (node.childCount === 0) {
50
+ if (rules.operatorLeafTypes.has(node.type)) {
52
51
  operators.set(node.type, (operators.get(node.type) || 0) + 1);
53
- }
54
-
55
- // Leaf nodes: classify as operator or operand
56
- if (node.childCount === 0) {
57
- if (rules?.operatorLeafTypes.has(node.type)) {
58
- operators.set(node.type, (operators.get(node.type) || 0) + 1);
59
- } else if (rules?.operandLeafTypes.has(node.type)) {
60
- const text = node.text;
61
- operands.set(text, (operands.get(text) || 0) + 1);
62
- }
63
- }
64
-
65
- for (let i = 0; i < node.childCount; i++) {
66
- walk(node.child(i));
52
+ } else if (rules.operandLeafTypes.has(node.type)) {
53
+ const text = node.text;
54
+ operands.set(text, (operands.get(text) || 0) + 1);
67
55
  }
68
56
  }
57
+ }
69
58
 
70
- walk(functionNode);
71
-
59
+ /** Build a HalsteadDerivedMetrics summary from the raw operator/operand counts. */
60
+ function summarizeHalsteadCounts(
61
+ operators: Map<string, number>,
62
+ operands: Map<string, number>,
63
+ ): HalsteadDerivedMetrics {
72
64
  const n1 = operators.size; // distinct operators
73
65
  const n2 = operands.size; // distinct operands
74
66
  let bigN1 = 0; // total operators
@@ -79,7 +71,6 @@ export function computeHalsteadMetrics(
79
71
  const vocabulary = n1 + n2;
80
72
  const length = bigN1 + bigN2;
81
73
 
82
- // Guard against zero
83
74
  const volume = vocabulary > 0 ? length * Math.log2(vocabulary) : 0;
84
75
  const difficulty = n2 > 0 ? (n1 / 2) * (bigN2 / n2) : 0;
85
76
  const effort = difficulty * volume;
@@ -99,6 +90,31 @@ export function computeHalsteadMetrics(
99
90
  };
100
91
  }
101
92
 
93
+ export function computeHalsteadMetrics(
94
+ functionNode: TreeSitterNode,
95
+ language: string,
96
+ ): HalsteadDerivedMetrics | null {
97
+ const rules = HALSTEAD_RULES.get(language) as HalsteadRules | undefined;
98
+ if (!rules) return null;
99
+
100
+ const operators = new Map<string, number>(); // type -> count
101
+ const operands = new Map<string, number>(); // text -> count
102
+
103
+ function walk(node: TreeSitterNode | null): void {
104
+ if (!node) return;
105
+ // Skip type annotation subtrees
106
+ if (rules?.skipTypes.has(node.type)) return;
107
+ classifyHalsteadToken(node, rules as HalsteadRules, operators, operands);
108
+ for (let i = 0; i < node.childCount; i++) {
109
+ walk(node.child(i));
110
+ }
111
+ }
112
+
113
+ walk(functionNode);
114
+
115
+ return summarizeHalsteadCounts(operators, operands);
116
+ }
117
+
102
118
  // ─── LOC Metrics Computation ──────────────────────────────────────────────
103
119
  // Delegated to ast-analysis/metrics.js; re-exported for backward compatibility.
104
120
  export const computeLOCMetrics = _computeLOCMetrics;
@@ -535,6 +551,89 @@ function upsertAstComplexity(
535
551
  return 1;
536
552
  }
537
553
 
554
+ /** Decision outcome for a single definition during native bulk-row collection.
555
+ * - 'skip': the definition is legitimately ignorable (non-function, missing line,
556
+ * interface stub, unsupported language).
557
+ * - 'fallback': a genuine function body is missing precomputed complexity —
558
+ * the whole native fast path must abort to JS.
559
+ * - 'emit': the definition has complexity data and a row was (or will be) appended. */
560
+ type NativeRowDecision = 'skip' | 'fallback' | 'emit';
561
+
562
+ /** Classify a definition relative to the native bulk path. Returns
563
+ * 'skip' to ignore it, 'fallback' to bail out, or 'emit' if the row should be added. */
564
+ function classifyDefinitionForNativeBulk(
565
+ def: FileSymbols['definitions'][0],
566
+ langSupported: boolean,
567
+ ): NativeRowDecision {
568
+ if (def.kind !== 'function' && def.kind !== 'method') return 'skip';
569
+ if (!def.line) return 'skip';
570
+ if (!def.complexity) {
571
+ // Interface/type property signatures and single-line stubs are extracted
572
+ // as methods but the native engine correctly never assigns complexity.
573
+ // Mirror the leniency in initWasmParsersIfNeeded to avoid bailing out
574
+ // of the native bulk-insert path for every TypeScript codebase (#846).
575
+ if (def.name.includes('.') || !def.endLine || def.endLine <= def.line) return 'skip';
576
+ // Languages without complexity rules will never have data — skip them
577
+ // rather than bailing out of the entire native bulk path.
578
+ if (!langSupported) return 'skip';
579
+ return 'fallback'; // genuine function body missing complexity — needs JS fallback
580
+ }
581
+ return 'emit';
582
+ }
583
+
584
+ /** Build a single native-bulk row from a definition with complexity data. */
585
+ function buildNativeBulkRow(
586
+ nodeId: number,
587
+ def: FileSymbols['definitions'][0],
588
+ ): Record<string, unknown> {
589
+ const ch = def.complexity?.halstead;
590
+ const cl = def.complexity?.loc;
591
+ return {
592
+ nodeId,
593
+ cognitive: def.complexity?.cognitive ?? 0,
594
+ cyclomatic: def.complexity?.cyclomatic ?? 0,
595
+ maxNesting: def.complexity?.maxNesting ?? 0,
596
+ loc: cl ? cl.loc : 0,
597
+ sloc: cl ? cl.sloc : 0,
598
+ commentLines: cl ? cl.commentLines : 0,
599
+ halsteadN1: ch ? ch.n1 : 0,
600
+ halsteadN2: ch ? ch.n2 : 0,
601
+ halsteadBigN1: ch ? ch.bigN1 : 0,
602
+ halsteadBigN2: ch ? ch.bigN2 : 0,
603
+ halsteadVocabulary: ch ? ch.vocabulary : 0,
604
+ halsteadLength: ch ? ch.length : 0,
605
+ halsteadVolume: ch ? ch.volume : 0,
606
+ halsteadDifficulty: ch ? ch.difficulty : 0,
607
+ halsteadEffort: ch ? ch.effort : 0,
608
+ halsteadBugs: ch ? ch.bugs : 0,
609
+ maintainabilityIndex: def.complexity?.maintainabilityIndex ?? 0,
610
+ };
611
+ }
612
+
613
+ /** Try to collect a single file's definitions into native-bulk rows.
614
+ * Returns 'fallback' if any definition forces a JS fallback. */
615
+ function collectFileBulkRows(
616
+ db: BetterSqlite3Database,
617
+ relPath: string,
618
+ symbols: FileSymbols,
619
+ rows: Array<Record<string, unknown>>,
620
+ ): NativeRowDecision {
621
+ const ext = path.extname(relPath).toLowerCase();
622
+ const langId = symbols._langId || '';
623
+ const langSupported = COMPLEXITY_EXTENSIONS.has(ext) || COMPLEXITY_RULES.has(langId);
624
+
625
+ for (const def of symbols.definitions) {
626
+ const decision = classifyDefinitionForNativeBulk(def, langSupported);
627
+ if (decision === 'skip') continue;
628
+ if (decision === 'fallback') return 'fallback';
629
+
630
+ const nodeId = getFunctionNodeId(db, def.name, relPath, def.line);
631
+ if (!nodeId) continue;
632
+ rows.push(buildNativeBulkRow(nodeId, def));
633
+ }
634
+ return 'emit';
635
+ }
636
+
538
637
  /** Collect native bulk-insert rows from precomputed complexity data.
539
638
  * Returns the rows array, or null if any definition is missing complexity
540
639
  * (signalling that JS fallback is needed). */
@@ -543,53 +642,9 @@ function collectNativeBulkRows(
543
642
  fileSymbols: Map<string, FileSymbols>,
544
643
  ): Array<Record<string, unknown>> | null {
545
644
  const rows: Array<Record<string, unknown>> = [];
546
-
547
645
  for (const [relPath, symbols] of fileSymbols) {
548
- const ext = path.extname(relPath).toLowerCase();
549
- const langId = symbols._langId || '';
550
- const langSupported = COMPLEXITY_EXTENSIONS.has(ext) || COMPLEXITY_RULES.has(langId);
551
-
552
- for (const def of symbols.definitions) {
553
- if (def.kind !== 'function' && def.kind !== 'method') continue;
554
- if (!def.line) continue;
555
- // Interface/type property signatures and single-line stubs are extracted
556
- // as methods but the native engine correctly never assigns complexity.
557
- // Mirror the leniency in initWasmParsersIfNeeded to avoid bailing out
558
- // of the native bulk-insert path for every TypeScript codebase (#846).
559
- if (!def.complexity) {
560
- if (def.name.includes('.') || !def.endLine || def.endLine <= def.line) continue;
561
- // Languages without complexity rules will never have data — skip them
562
- // rather than bailing out of the entire native bulk path.
563
- if (!langSupported) continue;
564
- return null; // genuine function body missing complexity — needs JS fallback
565
- }
566
- const nodeId = getFunctionNodeId(db, def.name, relPath, def.line);
567
- if (!nodeId) continue;
568
- const ch = def.complexity.halstead;
569
- const cl = def.complexity.loc;
570
- rows.push({
571
- nodeId,
572
- cognitive: def.complexity.cognitive ?? 0,
573
- cyclomatic: def.complexity.cyclomatic ?? 0,
574
- maxNesting: def.complexity.maxNesting ?? 0,
575
- loc: cl ? cl.loc : 0,
576
- sloc: cl ? cl.sloc : 0,
577
- commentLines: cl ? cl.commentLines : 0,
578
- halsteadN1: ch ? ch.n1 : 0,
579
- halsteadN2: ch ? ch.n2 : 0,
580
- halsteadBigN1: ch ? ch.bigN1 : 0,
581
- halsteadBigN2: ch ? ch.bigN2 : 0,
582
- halsteadVocabulary: ch ? ch.vocabulary : 0,
583
- halsteadLength: ch ? ch.length : 0,
584
- halsteadVolume: ch ? ch.volume : 0,
585
- halsteadDifficulty: ch ? ch.difficulty : 0,
586
- halsteadEffort: ch ? ch.effort : 0,
587
- halsteadBugs: ch ? ch.bugs : 0,
588
- maintainabilityIndex: def.complexity.maintainabilityIndex ?? 0,
589
- });
590
- }
646
+ if (collectFileBulkRows(db, relPath, symbols, rows) === 'fallback') return null;
591
647
  }
592
-
593
648
  return rows;
594
649
  }
595
650
 
@@ -675,6 +675,51 @@ interface BfsParentEntry {
675
675
  expression: string;
676
676
  }
677
677
 
678
+ type DataflowNeighbor = {
679
+ id: number;
680
+ file: string;
681
+ edge_kind: string;
682
+ expression: string;
683
+ };
684
+
685
+ interface DataflowBfsState {
686
+ visited: Set<number>;
687
+ parent: Map<number, BfsParentEntry>;
688
+ nextQueue: number[];
689
+ found: boolean;
690
+ }
691
+
692
+ /**
693
+ * Process a single neighbor in the dataflow BFS. Returns true once the target
694
+ * has been reached so the caller can stop expanding.
695
+ */
696
+ function processDataflowNeighbor(
697
+ n: DataflowNeighbor,
698
+ currentId: number,
699
+ targetId: number,
700
+ noTests: boolean,
701
+ state: DataflowBfsState,
702
+ ): boolean {
703
+ if (noTests && isTestFile(n.file)) return false;
704
+ const entry: BfsParentEntry = {
705
+ parentId: currentId,
706
+ edgeKind: n.edge_kind,
707
+ expression: n.expression,
708
+ };
709
+ if (n.id === targetId) {
710
+ if (!state.found) {
711
+ state.found = true;
712
+ state.parent.set(n.id, entry);
713
+ }
714
+ return true;
715
+ }
716
+ if (state.visited.has(n.id)) return false;
717
+ state.visited.add(n.id);
718
+ state.parent.set(n.id, entry);
719
+ state.nextQueue.push(n.id);
720
+ return false;
721
+ }
722
+
678
723
  /** BFS through dataflow edges to find a path from source to target. */
679
724
  function bfsDataflowPath(
680
725
  db: BetterSqlite3Database,
@@ -689,50 +734,28 @@ function bfsDataflowPath(
689
734
  WHERE d.source_id = ? AND d.kind IN ('flows_to', 'returns')`,
690
735
  );
691
736
 
692
- const visited = new Set<number>([sourceId]);
693
- const parent = new Map<number, BfsParentEntry>();
737
+ const state: DataflowBfsState = {
738
+ visited: new Set<number>([sourceId]),
739
+ parent: new Map<number, BfsParentEntry>(),
740
+ nextQueue: [],
741
+ found: false,
742
+ };
694
743
  let queue = [sourceId];
695
- let found = false;
696
744
 
697
745
  for (let depth = 1; depth <= maxDepth; depth++) {
698
- const nextQueue: number[] = [];
746
+ state.nextQueue = [];
699
747
  for (const currentId of queue) {
700
- const neighbors = neighborStmt.all(currentId) as Array<{
701
- id: number;
702
- file: string;
703
- edge_kind: string;
704
- expression: string;
705
- }>;
748
+ const neighbors = neighborStmt.all(currentId) as DataflowNeighbor[];
706
749
  for (const n of neighbors) {
707
- if (noTests && isTestFile(n.file)) continue;
708
- if (n.id === targetId) {
709
- if (!found) {
710
- found = true;
711
- parent.set(n.id, {
712
- parentId: currentId,
713
- edgeKind: n.edge_kind,
714
- expression: n.expression,
715
- });
716
- }
717
- continue;
718
- }
719
- if (!visited.has(n.id)) {
720
- visited.add(n.id);
721
- parent.set(n.id, {
722
- parentId: currentId,
723
- edgeKind: n.edge_kind,
724
- expression: n.expression,
725
- });
726
- nextQueue.push(n.id);
727
- }
750
+ processDataflowNeighbor(n, currentId, targetId, noTests, state);
728
751
  }
729
752
  }
730
- if (found) break;
731
- queue = nextQueue;
753
+ if (state.found) break;
754
+ queue = state.nextQueue;
732
755
  if (queue.length === 0) break;
733
756
  }
734
757
 
735
- return found ? parent : null;
758
+ return state.found ? state.parent : null;
736
759
  }
737
760
 
738
761
  /** Reconstruct a path from BFS parent map. */
@@ -133,6 +133,41 @@ interface BfsState {
133
133
  truncated: boolean;
134
134
  }
135
135
 
136
+ interface FlowBfsFrame {
137
+ visited: Set<number>;
138
+ cycles: Array<{ from: string; to: string; depth: number }>;
139
+ nodeDepths: Map<number, number>;
140
+ idToNode: Map<number, NodeInfo>;
141
+ nextFrontier: number[];
142
+ levelNodes: NodeInfo[];
143
+ }
144
+
145
+ /** Process one callee row, recording cycle hits or expanding frontier. */
146
+ function processFlowCallee(
147
+ c: CalleeRow,
148
+ fid: number,
149
+ depth: number,
150
+ noTests: boolean,
151
+ frame: FlowBfsFrame,
152
+ ): void {
153
+ if (noTests && isTestFile(c.file)) return;
154
+
155
+ if (frame.visited.has(c.id)) {
156
+ const fromNode = frame.idToNode.get(fid);
157
+ if (fromNode) {
158
+ frame.cycles.push({ from: fromNode.name, to: c.name, depth });
159
+ }
160
+ return;
161
+ }
162
+
163
+ frame.visited.add(c.id);
164
+ frame.nextFrontier.push(c.id);
165
+ const nodeInfo: NodeInfo = toSymbolRef(c);
166
+ frame.levelNodes.push(nodeInfo);
167
+ frame.nodeDepths.set(c.id, depth);
168
+ frame.idToNode.set(c.id, nodeInfo);
169
+ }
170
+
136
171
  /** Forward BFS through callees, collecting steps, cycles, and node depth info. */
137
172
  function bfsCallees(
138
173
  db: ReturnType<typeof openReadonlyOrFail>,
@@ -157,37 +192,26 @@ function bfsCallees(
157
192
  );
158
193
 
159
194
  for (let d = 1; d <= maxDepth; d++) {
160
- const nextFrontier: number[] = [];
161
- const levelNodes: NodeInfo[] = [];
195
+ const frame: FlowBfsFrame = {
196
+ visited,
197
+ cycles,
198
+ nodeDepths,
199
+ idToNode,
200
+ nextFrontier: [],
201
+ levelNodes: [],
202
+ };
162
203
 
163
204
  for (const fid of frontier) {
164
- const callees = calleesStmt.all(fid);
165
-
166
- for (const c of callees) {
167
- if (noTests && isTestFile(c.file)) continue;
168
-
169
- if (visited.has(c.id)) {
170
- const fromNode = idToNode.get(fid);
171
- if (fromNode) {
172
- cycles.push({ from: fromNode.name, to: c.name, depth: d });
173
- }
174
- continue;
175
- }
176
-
177
- visited.add(c.id);
178
- nextFrontier.push(c.id);
179
- const nodeInfo: NodeInfo = toSymbolRef(c);
180
- levelNodes.push(nodeInfo);
181
- nodeDepths.set(c.id, d);
182
- idToNode.set(c.id, nodeInfo);
205
+ for (const c of calleesStmt.all(fid)) {
206
+ processFlowCallee(c, fid, d, noTests, frame);
183
207
  }
184
208
  }
185
209
 
186
- if (levelNodes.length > 0) {
187
- steps.push({ depth: d, nodes: levelNodes });
210
+ if (frame.levelNodes.length > 0) {
211
+ steps.push({ depth: d, nodes: frame.levelNodes });
188
212
  }
189
213
 
190
- frontier = nextFrontier;
214
+ frontier = frame.nextFrontier;
191
215
  if (frontier.length === 0) break;
192
216
  if (d === maxDepth && frontier.length > 0) truncated = true;
193
217
  }
@@ -336,13 +336,13 @@ interface FileLevelEdge {
336
336
  target: string;
337
337
  }
338
338
 
339
- function prepareFileLevelData(
339
+ /** Load file-level import/call edges from the DB and optionally exclude test files. */
340
+ function loadFileLevelEdges(
340
341
  db: BetterSqlite3Database,
341
342
  noTests: boolean,
342
343
  minConf: number,
343
- cfg: PlotConfig,
344
- ): GraphData {
345
- let edges = db
344
+ ): FileLevelEdge[] {
345
+ const edges = db
346
346
  .prepare<FileLevelEdge>(
347
347
  `
348
348
  SELECT DISTINCT n1.file AS source, n2.file AS target
@@ -354,73 +354,118 @@ function prepareFileLevelData(
354
354
  `,
355
355
  )
356
356
  .all(minConf);
357
- if (noTests) edges = edges.filter((e) => !isTestFile(e.source) && !isTestFile(e.target));
358
-
359
- const files = new Set<string>();
360
- for (const { source, target } of edges) {
361
- files.add(source);
362
- files.add(target);
363
- }
364
-
365
- const fileIds = new Map<string, number>();
366
- let idx = 0;
367
- for (const f of files) fileIds.set(f, idx++);
357
+ return noTests ? edges.filter((e) => !isTestFile(e.source) && !isTestFile(e.target)) : edges;
358
+ }
368
359
 
369
- // Fan-in/fan-out
360
+ /** Compute fan-in and fan-out for each file from a list of edges. */
361
+ function computeFileFanCounts(edges: FileLevelEdge[]): {
362
+ fanInCount: Map<string, number>;
363
+ fanOutCount: Map<string, number>;
364
+ } {
370
365
  const fanInCount = new Map<string, number>();
371
366
  const fanOutCount = new Map<string, number>();
372
367
  for (const { source, target } of edges) {
373
368
  fanOutCount.set(source, (fanOutCount.get(source) || 0) + 1);
374
369
  fanInCount.set(target, (fanInCount.get(target) || 0) + 1);
375
370
  }
371
+ return { fanInCount, fanOutCount };
372
+ }
376
373
 
377
- // Communities via graph subsystem
374
+ /** Run Louvain community detection on the file-level graph. Returns empty map on failure. */
375
+ function detectFileCommunities(files: Set<string>, edges: FileLevelEdge[]): Map<string, number> {
378
376
  const communityMap = new Map<string, number>();
379
- if (files.size > 0) {
380
- try {
381
- const fileGraph = new CodeGraph();
382
- for (const f of files) fileGraph.addNode(f);
383
- for (const { source, target } of edges) {
384
- if (source !== target && !fileGraph.hasEdge(source, target))
385
- fileGraph.addEdge(source, target);
386
- }
387
- const { assignments } = louvainCommunities(fileGraph);
388
- for (const [file, cid] of assignments) communityMap.set(file, cid);
389
- } catch {
390
- // ignore
377
+ if (files.size === 0) return communityMap;
378
+ try {
379
+ const fileGraph = new CodeGraph();
380
+ for (const f of files) fileGraph.addNode(f);
381
+ for (const { source, target } of edges) {
382
+ if (source !== target && !fileGraph.hasEdge(source, target))
383
+ fileGraph.addEdge(source, target);
391
384
  }
385
+ const { assignments } = louvainCommunities(fileGraph);
386
+ for (const [file, cid] of assignments) communityMap.set(file, cid);
387
+ } catch {
388
+ // louvain can fail on disconnected graphs
392
389
  }
390
+ return communityMap;
391
+ }
393
392
 
394
- const visNodes: VisNode[] = [...files].map((f) => {
395
- const id = fileIds.get(f)!;
396
- const community = communityMap.get(f) ?? null;
397
- const fanIn = fanInCount.get(f) || 0;
398
- const fanOut = fanOutCount.get(f) || 0;
399
- const directory = path.dirname(f);
400
- const color: string =
401
- cfg.colorBy === 'community' && community !== null
402
- ? COMMUNITY_COLORS[community % COMMUNITY_COLORS.length] || '#ccc'
403
- : cfg.nodeColors?.file || (DEFAULT_NODE_COLORS as Record<string, string>).file || '#ccc';
404
-
405
- return {
406
- id,
407
- label: path.basename(f),
408
- title: f,
409
- color,
410
- kind: 'file',
411
- role: '',
412
- file: f,
413
- line: 0,
414
- community,
415
- cognitive: null,
416
- cyclomatic: null,
417
- maintainabilityIndex: null,
418
- fanIn,
419
- fanOut,
420
- directory,
421
- risk: [],
422
- };
423
- });
393
+ /** Build a VisNode for a single file, applying color based on cfg.colorBy. */
394
+ function buildFileVisNode(
395
+ file: string,
396
+ id: number,
397
+ community: number | null,
398
+ fanIn: number,
399
+ fanOut: number,
400
+ cfg: PlotConfig,
401
+ ): VisNode {
402
+ const color: string =
403
+ cfg.colorBy === 'community' && community !== null
404
+ ? COMMUNITY_COLORS[community % COMMUNITY_COLORS.length] || '#ccc'
405
+ : cfg.nodeColors?.file || (DEFAULT_NODE_COLORS as Record<string, string>).file || '#ccc';
406
+
407
+ return {
408
+ id,
409
+ label: path.basename(file),
410
+ title: file,
411
+ color,
412
+ kind: 'file',
413
+ role: '',
414
+ file,
415
+ line: 0,
416
+ community,
417
+ cognitive: null,
418
+ cyclomatic: null,
419
+ maintainabilityIndex: null,
420
+ fanIn,
421
+ fanOut,
422
+ directory: path.dirname(file),
423
+ risk: [],
424
+ };
425
+ }
426
+
427
+ /** Select seed node IDs for the file-level graph based on configured strategy. */
428
+ function selectFileSeedNodes(visNodes: VisNode[], cfg: PlotConfig): (number | string)[] {
429
+ if (cfg.seedStrategy === 'top-fanin') {
430
+ const sorted = [...visNodes].sort((a, b) => b.fanIn - a.fanIn);
431
+ return sorted.slice(0, cfg.seedCount || 30).map((n) => n.id);
432
+ }
433
+ // Both 'entry' and the default fallback include every node — file-level graphs
434
+ // don't track per-file roles, so 'entry' has no meaningful filter.
435
+ return visNodes.map((n) => n.id);
436
+ }
437
+
438
+ function prepareFileLevelData(
439
+ db: BetterSqlite3Database,
440
+ noTests: boolean,
441
+ minConf: number,
442
+ cfg: PlotConfig,
443
+ ): GraphData {
444
+ const edges = loadFileLevelEdges(db, noTests, minConf);
445
+
446
+ const files = new Set<string>();
447
+ for (const { source, target } of edges) {
448
+ files.add(source);
449
+ files.add(target);
450
+ }
451
+
452
+ const fileIds = new Map<string, number>();
453
+ let idx = 0;
454
+ for (const f of files) fileIds.set(f, idx++);
455
+
456
+ const { fanInCount, fanOutCount } = computeFileFanCounts(edges);
457
+ const communityMap = detectFileCommunities(files, edges);
458
+
459
+ const visNodes: VisNode[] = [...files].map((f) =>
460
+ buildFileVisNode(
461
+ f,
462
+ fileIds.get(f)!,
463
+ communityMap.get(f) ?? null,
464
+ fanInCount.get(f) || 0,
465
+ fanOutCount.get(f) || 0,
466
+ cfg,
467
+ ),
468
+ );
424
469
 
425
470
  const visEdges: VisEdge[] = edges.map(({ source, target }, i) => ({
426
471
  id: `e${i}`,
@@ -428,17 +473,7 @@ function prepareFileLevelData(
428
473
  to: fileIds.get(target)!,
429
474
  }));
430
475
 
431
- let seedNodeIds: (number | string)[];
432
- if (cfg.seedStrategy === 'top-fanin') {
433
- const sorted = [...visNodes].sort((a, b) => b.fanIn - a.fanIn);
434
- seedNodeIds = sorted.slice(0, cfg.seedCount || 30).map((n) => n.id);
435
- } else if (cfg.seedStrategy === 'entry') {
436
- seedNodeIds = visNodes.map((n) => n.id);
437
- } else {
438
- seedNodeIds = visNodes.map((n) => n.id);
439
- }
440
-
441
- return { nodes: visNodes, edges: visEdges, seedNodeIds };
476
+ return { nodes: visNodes, edges: visEdges, seedNodeIds: selectFileSeedNodes(visNodes, cfg) };
442
477
  }
443
478
 
444
479
  // ─── HTML Generation (thin wrapper) ──────────────────────────────────