@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
@@ -8,6 +8,7 @@ import { kindIcon } from '../domain/queries.js';
8
8
  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
+ import { toErrorMessage } from '../shared/errors.js';
11
12
  import type { EngineMode, NativeDatabase } from '../types.js';
12
13
 
13
14
  // ─── Git Helpers ────────────────────────────────────────────────────────
@@ -20,7 +21,8 @@ function validateGitRef(repoRoot: string, ref: string): string | null {
20
21
  stdio: ['pipe', 'pipe', 'pipe'],
21
22
  }).trim();
22
23
  return sha;
23
- } catch {
24
+ } catch (e) {
25
+ debug(`validateGitRef failed for "${ref}": ${toErrorMessage(e)}`);
24
26
  return null;
25
27
  }
26
28
  }
@@ -50,11 +52,12 @@ function removeWorktree(repoRoot: string, dir: string): void {
50
52
  encoding: 'utf-8',
51
53
  stdio: ['pipe', 'pipe', 'pipe'],
52
54
  });
53
- } catch {
55
+ } catch (e) {
56
+ debug(`removeWorktree: git worktree remove failed for ${dir}: ${toErrorMessage(e)}`);
54
57
  try {
55
58
  fs.rmSync(dir, { recursive: true, force: true });
56
- } catch {
57
- /* best-effort */
59
+ } catch (rmErr) {
60
+ debug(`removeWorktree: rmSync fallback failed for ${dir}: ${toErrorMessage(rmErr)}`);
58
61
  }
59
62
  try {
60
63
  execFileSync('git', ['worktree', 'prune'], {
@@ -62,8 +65,8 @@ function removeWorktree(repoRoot: string, dir: string): void {
62
65
  encoding: 'utf-8',
63
66
  stdio: ['pipe', 'pipe', 'pipe'],
64
67
  });
65
- } catch {
66
- /* best-effort */
68
+ } catch (pruneErr) {
69
+ debug(`removeWorktree: git worktree prune failed: ${toErrorMessage(pruneErr)}`);
67
70
  }
68
71
  }
69
72
  }
@@ -117,7 +120,7 @@ function loadSymbolsFromDb(
117
120
  const native = getNative();
118
121
  nativeDb = native.NativeDatabase.openReadonly(dbPath);
119
122
  } catch (e) {
120
- debug(`loadSymbolsFromDb: native path failed: ${(e as Error).message}`);
123
+ debug(`loadSymbolsFromDb: native path failed: ${toErrorMessage(e)}`);
121
124
  }
122
125
  }
123
126
 
@@ -205,8 +208,8 @@ function loadSymbolsFromDb(
205
208
  if (nativeDb) {
206
209
  try {
207
210
  nativeDb.close();
208
- } catch {
209
- /* already closed */
211
+ } catch (e) {
212
+ debug(`loadSymbolsFromDb: nativeDb close failed: ${toErrorMessage(e)}`);
210
213
  }
211
214
  }
212
215
  }
@@ -360,6 +363,38 @@ interface BranchCompareResult {
360
363
  summary?: BranchCompareSummary;
361
364
  }
362
365
 
366
+ function attachImpactToSymbols(
367
+ symbols: SymbolInfo[],
368
+ dbPath: string,
369
+ _baseSymbols: Map<string, SymbolInfo>,
370
+ maxDepth: number,
371
+ noTests: boolean,
372
+ ): void {
373
+ for (const sym of symbols) {
374
+ const symCallers = loadCallersFromDb(dbPath, sym.id ? [sym.id] : [], maxDepth, noTests);
375
+ (sym as SymbolInfo & { impact?: CallerInfo[] }).impact = symCallers;
376
+ }
377
+ }
378
+
379
+ function attachImpactToChanged(
380
+ changed: ChangedSymbol[],
381
+ dbPath: string,
382
+ baseSymbols: Map<string, SymbolInfo>,
383
+ maxDepth: number,
384
+ noTests: boolean,
385
+ ): void {
386
+ for (const sym of changed) {
387
+ const baseSym = baseSymbols.get(makeSymbolKey(sym.kind, sym.file, sym.name));
388
+ const symCallers = loadCallersFromDb(
389
+ dbPath,
390
+ baseSym?.id ? [baseSym.id] : [],
391
+ maxDepth,
392
+ noTests,
393
+ );
394
+ sym.impact = symCallers;
395
+ }
396
+ }
397
+
363
398
  export async function branchCompareData(
364
399
  baseRef: string,
365
400
  targetRef: string,
@@ -376,7 +411,8 @@ export async function branchCompareData(
376
411
  encoding: 'utf-8',
377
412
  stdio: ['pipe', 'pipe', 'pipe'],
378
413
  });
379
- } catch {
414
+ } catch (e) {
415
+ debug(`branchCompareData: git check failed: ${toErrorMessage(e)}`);
380
416
  return { error: 'Not a git repository' };
381
417
  }
382
418
 
@@ -440,20 +476,8 @@ export async function branchCompareData(
440
476
  const removedImpact = loadCallersFromDb(baseDbPath, removedIds, maxDepth, noTests);
441
477
  const changedImpact = loadCallersFromDb(baseDbPath, changedIds, maxDepth, noTests);
442
478
 
443
- for (const sym of removed) {
444
- const symCallers = loadCallersFromDb(baseDbPath, sym.id ? [sym.id] : [], maxDepth, noTests);
445
- (sym as SymbolInfo & { impact?: CallerInfo[] }).impact = symCallers;
446
- }
447
- for (const sym of changed) {
448
- const baseSym = baseSymbols.get(makeSymbolKey(sym.kind, sym.file, sym.name));
449
- const symCallers = loadCallersFromDb(
450
- baseDbPath,
451
- baseSym?.id ? [baseSym.id] : [],
452
- maxDepth,
453
- noTests,
454
- );
455
- sym.impact = symCallers;
456
- }
479
+ attachImpactToSymbols(removed, baseDbPath, baseSymbols, maxDepth, noTests);
480
+ attachImpactToChanged(changed, baseDbPath, baseSymbols, maxDepth, noTests);
457
481
 
458
482
  const allImpacted = new Set<string>();
459
483
  for (const c of removedImpact) allImpacted.add(`${c.file}:${c.name}`);
@@ -489,20 +513,66 @@ export async function branchCompareData(
489
513
  },
490
514
  };
491
515
  } catch (err) {
492
- return { error: (err as Error).message };
516
+ return { error: toErrorMessage(err) };
493
517
  } finally {
494
518
  removeWorktree(repoRoot, baseDir);
495
519
  removeWorktree(repoRoot, targetDir);
496
520
  try {
497
521
  fs.rmSync(tmpBase, { recursive: true, force: true });
498
- } catch {
499
- /* best-effort */
522
+ } catch (cleanupErr) {
523
+ debug(`branchCompareData: temp cleanup failed: ${toErrorMessage(cleanupErr)}`);
500
524
  }
501
525
  }
502
526
  }
503
527
 
504
528
  // ─── Mermaid Output ─────────────────────────────────────────────────────
505
529
 
530
+ interface MermaidNodeIdState {
531
+ counter: number;
532
+ map: Map<string, string>;
533
+ }
534
+
535
+ function mermaidNodeId(state: MermaidNodeIdState, key: string): string {
536
+ if (!state.map.has(key)) {
537
+ state.map.set(key, `n${state.counter++}`);
538
+ }
539
+ return state.map.get(key)!;
540
+ }
541
+
542
+ function addMermaidSubgraph(
543
+ lines: string[],
544
+ state: MermaidNodeIdState,
545
+ prefix: string,
546
+ label: string,
547
+ symbols: Array<{ kind: string; file: string; name: string }>,
548
+ fillColor: string,
549
+ strokeColor: string,
550
+ ): void {
551
+ if (symbols.length === 0) return;
552
+ lines.push(` subgraph sg_${prefix}["${label}"]`);
553
+ for (const sym of symbols) {
554
+ const key = `${prefix}::${sym.kind}::${sym.file}::${sym.name}`;
555
+ const nid = mermaidNodeId(state, key);
556
+ lines.push(` ${nid}["[${kindIcon(sym.kind)}] ${sym.name}"]`);
557
+ }
558
+ lines.push(' end');
559
+ lines.push(` style sg_${prefix} fill:${fillColor},stroke:${strokeColor}`);
560
+ }
561
+
562
+ function collectImpactedCallers(
563
+ impactSources: Array<{ impact?: CallerInfo[] }>,
564
+ ): Map<string, CallerInfo> {
565
+ const allImpacted = new Map<string, CallerInfo>();
566
+ for (const sym of impactSources) {
567
+ if (!sym.impact) continue;
568
+ for (const c of sym.impact) {
569
+ const key = `impact::${c.kind}::${c.file}::${c.name}`;
570
+ if (!allImpacted.has(key)) allImpacted.set(key, c);
571
+ }
572
+ }
573
+ return allImpacted;
574
+ }
575
+
506
576
  export function branchCompareMermaid(data: BranchCompareResult): string {
507
577
  if (data.error) return data.error;
508
578
  if (
@@ -514,63 +584,19 @@ export function branchCompareMermaid(data: BranchCompareResult): string {
514
584
  }
515
585
 
516
586
  const lines = ['flowchart TB'];
517
- let nodeCounter = 0;
518
- const nodeIdMap = new Map<string, string>();
519
-
520
- function nodeId(key: string): string {
521
- if (!nodeIdMap.has(key)) {
522
- nodeIdMap.set(key, `n${nodeCounter++}`);
523
- }
524
- return nodeIdMap.get(key)!;
525
- }
526
-
527
- if (data.added && data.added.length > 0) {
528
- lines.push(' subgraph sg_added["Added"]');
529
- for (const sym of data.added) {
530
- const key = `added::${sym.kind}::${sym.file}::${sym.name}`;
531
- const nid = nodeId(key);
532
- lines.push(` ${nid}["[${kindIcon(sym.kind)}] ${sym.name}"]`);
533
- }
534
- lines.push(' end');
535
- lines.push(' style sg_added fill:#e8f5e9,stroke:#4caf50');
536
- }
537
-
538
- if (data.removed && data.removed.length > 0) {
539
- lines.push(' subgraph sg_removed["Removed"]');
540
- for (const sym of data.removed) {
541
- const key = `removed::${sym.kind}::${sym.file}::${sym.name}`;
542
- const nid = nodeId(key);
543
- lines.push(` ${nid}["[${kindIcon(sym.kind)}] ${sym.name}"]`);
544
- }
545
- lines.push(' end');
546
- lines.push(' style sg_removed fill:#ffebee,stroke:#f44336');
547
- }
587
+ const state: MermaidNodeIdState = { counter: 0, map: new Map() };
548
588
 
549
- if (data.changed && data.changed.length > 0) {
550
- lines.push(' subgraph sg_changed["Changed"]');
551
- for (const sym of data.changed) {
552
- const key = `changed::${sym.kind}::${sym.file}::${sym.name}`;
553
- const nid = nodeId(key);
554
- lines.push(` ${nid}["[${kindIcon(sym.kind)}] ${sym.name}"]`);
555
- }
556
- lines.push(' end');
557
- lines.push(' style sg_changed fill:#fff3e0,stroke:#ff9800');
558
- }
589
+ addMermaidSubgraph(lines, state, 'added', 'Added', data.added || [], '#e8f5e9', '#4caf50');
590
+ addMermaidSubgraph(lines, state, 'removed', 'Removed', data.removed || [], '#ffebee', '#f44336');
591
+ addMermaidSubgraph(lines, state, 'changed', 'Changed', data.changed || [], '#fff3e0', '#ff9800');
559
592
 
560
- const allImpacted = new Map<string, CallerInfo>();
561
593
  const impactSources = [...(data.removed || []), ...(data.changed || [])];
562
- for (const sym of impactSources) {
563
- if (!sym.impact) continue;
564
- for (const c of sym.impact) {
565
- const key = `impact::${c.kind}::${c.file}::${c.name}`;
566
- if (!allImpacted.has(key)) allImpacted.set(key, c);
567
- }
568
- }
594
+ const allImpacted = collectImpactedCallers(impactSources);
569
595
 
570
596
  if (allImpacted.size > 0) {
571
597
  lines.push(' subgraph sg_impact["Impacted Callers"]');
572
598
  for (const [key, c] of allImpacted) {
573
- const nid = nodeId(key);
599
+ const nid = mermaidNodeId(state, key);
574
600
  lines.push(` ${nid}["[${kindIcon(c.kind)}] ${c.name}"]`);
575
601
  }
576
602
  lines.push(' end');
@@ -583,8 +609,8 @@ export function branchCompareMermaid(data: BranchCompareResult): string {
583
609
  const symKey = `${prefix}::${sym.kind}::${sym.file}::${sym.name}`;
584
610
  for (const c of sym.impact) {
585
611
  const callerKey = `impact::${c.kind}::${c.file}::${c.name}`;
586
- if (nodeIdMap.has(symKey) && nodeIdMap.has(callerKey)) {
587
- lines.push(` ${nodeIdMap.get(symKey)} -.-> ${nodeIdMap.get(callerKey)}`);
612
+ if (state.map.has(symKey) && state.map.has(callerKey)) {
613
+ lines.push(` ${state.map.get(symKey)} -.-> ${state.map.get(callerKey)}`);
588
614
  }
589
615
  }
590
616
  }
@@ -86,10 +86,7 @@ interface DriftResult {
86
86
  driftScore: number;
87
87
  }
88
88
 
89
- function analyzeDrift(
90
- communities: CommunityObject[],
91
- communityDirs: Map<number, Set<string>>,
92
- ): DriftResult {
89
+ function buildDirToCommunities(communityDirs: Map<number, Set<string>>): Map<string, Set<number>> {
93
90
  const dirToCommunities = new Map<string, Set<number>>();
94
91
  for (const [cid, dirs] of communityDirs) {
95
92
  for (const dir of dirs) {
@@ -97,20 +94,28 @@ function analyzeDrift(
97
94
  dirToCommunities.get(dir)!.add(cid);
98
95
  }
99
96
  }
97
+ return dirToCommunities;
98
+ }
100
99
 
101
- const splitCandidates: DriftResult['splitCandidates'] = [];
100
+ function findSplitCandidates(
101
+ dirToCommunities: Map<string, Set<number>>,
102
+ ): DriftResult['splitCandidates'] {
103
+ const candidates: DriftResult['splitCandidates'] = [];
102
104
  for (const [dir, cids] of dirToCommunities) {
103
105
  if (cids.size >= 2) {
104
- splitCandidates.push({ directory: dir, communityCount: cids.size });
106
+ candidates.push({ directory: dir, communityCount: cids.size });
105
107
  }
106
108
  }
107
- splitCandidates.sort((a, b) => b.communityCount - a.communityCount);
109
+ candidates.sort((a, b) => b.communityCount - a.communityCount);
110
+ return candidates;
111
+ }
108
112
 
109
- const mergeCandidates: DriftResult['mergeCandidates'] = [];
113
+ function findMergeCandidates(communities: CommunityObject[]): DriftResult['mergeCandidates'] {
114
+ const candidates: DriftResult['mergeCandidates'] = [];
110
115
  for (const c of communities) {
111
116
  const dirCount = Object.keys(c.directories).length;
112
117
  if (dirCount >= 2) {
113
- mergeCandidates.push({
118
+ candidates.push({
114
119
  communityId: c.id,
115
120
  size: c.size,
116
121
  directoryCount: dirCount,
@@ -118,7 +123,17 @@ function analyzeDrift(
118
123
  });
119
124
  }
120
125
  }
121
- mergeCandidates.sort((a, b) => b.directoryCount - a.directoryCount);
126
+ candidates.sort((a, b) => b.directoryCount - a.directoryCount);
127
+ return candidates;
128
+ }
129
+
130
+ function analyzeDrift(
131
+ communities: CommunityObject[],
132
+ communityDirs: Map<number, Set<string>>,
133
+ ): DriftResult {
134
+ const dirToCommunities = buildDirToCommunities(communityDirs);
135
+ const splitCandidates = findSplitCandidates(dirToCommunities);
136
+ const mergeCandidates = findMergeCandidates(communities);
122
137
 
123
138
  const totalDirs = dirToCommunities.size;
124
139
  const splitRatio = totalDirs > 0 ? splitCandidates.length / totalDirs : 0;
@@ -109,6 +109,163 @@ export const computeMaintainabilityIndex = _computeMaintainabilityIndex;
109
109
 
110
110
  // ─── Algorithm: Single-Traversal DFS ──────────────────────────────────────
111
111
 
112
+ interface ComplexityAccumulator {
113
+ cognitive: number;
114
+ cyclomatic: number;
115
+ maxNesting: number;
116
+ }
117
+
118
+ type WalkFn = (n: TreeSitterNode | null, level: number, isTop: boolean) => void;
119
+
120
+ /** Walk all children at the given nesting level. */
121
+ function walkChildren(node: TreeSitterNode, nestingLevel: number, walkFn: WalkFn): void {
122
+ for (let i = 0; i < node.childCount; i++) {
123
+ walkFn(node.child(i), nestingLevel, false);
124
+ }
125
+ }
126
+
127
+ /** Handle logical operators in binary expressions. Returns true if handled. */
128
+ function handleLogicalOperator(
129
+ node: TreeSitterNode,
130
+ type: string,
131
+ rules: ComplexityRules,
132
+ acc: ComplexityAccumulator,
133
+ nestingLevel: number,
134
+ walkFn: WalkFn,
135
+ ): boolean {
136
+ if (type !== rules.logicalNodeType) return false;
137
+
138
+ const op = node.child(1)?.type;
139
+ if (!op || !rules.logicalOperators.has(op)) return false;
140
+
141
+ acc.cyclomatic++;
142
+
143
+ // Cognitive: +1 only when operator changes from the previous sibling sequence
144
+ const parent = node.parent;
145
+ const sameSequence = parent?.type === rules.logicalNodeType && parent.child(1)?.type === op;
146
+ if (!sameSequence) acc.cognitive++;
147
+
148
+ walkChildren(node, nestingLevel, walkFn);
149
+ return true;
150
+ }
151
+
152
+ /** Handle else clause wrapping an if (Pattern A: JS/C#/Rust). Returns true if handled. */
153
+ function handleElseClause(
154
+ node: TreeSitterNode,
155
+ type: string,
156
+ rules: ComplexityRules,
157
+ acc: ComplexityAccumulator,
158
+ nestingLevel: number,
159
+ walkFn: WalkFn,
160
+ ): boolean {
161
+ if (!rules.elseNodeType || type !== rules.elseNodeType) return false;
162
+
163
+ const firstChild = node.namedChild(0);
164
+ if (firstChild && firstChild.type === rules.ifNodeType) {
165
+ // else-if: the if_statement child handles its own increment
166
+ walkChildren(node, nestingLevel, walkFn);
167
+ return true;
168
+ }
169
+ // Plain else
170
+ acc.cognitive++;
171
+ walkChildren(node, nestingLevel, walkFn);
172
+ return true;
173
+ }
174
+
175
+ /** Detect and handle else-if patterns (Patterns A, B, C). Returns true if handled. */
176
+ function handleElseIf(
177
+ node: TreeSitterNode,
178
+ type: string,
179
+ rules: ComplexityRules,
180
+ acc: ComplexityAccumulator,
181
+ nestingLevel: number,
182
+ walkFn: WalkFn,
183
+ ): boolean {
184
+ // Pattern B: explicit elif node (Python/Ruby/PHP)
185
+ if (rules.elifNodeType && type === rules.elifNodeType) {
186
+ acc.cognitive++;
187
+ acc.cyclomatic++;
188
+ walkChildren(node, nestingLevel, walkFn);
189
+ return true;
190
+ }
191
+
192
+ // Detect else-if via Pattern A or C
193
+ if (type === rules.ifNodeType) {
194
+ let isElseIf = false;
195
+ if (rules.elseViaAlternative) {
196
+ // Pattern C (Go/Java): if_statement is the alternative of parent if_statement
197
+ isElseIf =
198
+ node.parent?.type === rules.ifNodeType &&
199
+ node.parent.childForFieldName('alternative')?.id === node.id;
200
+ } else if (rules.elseNodeType) {
201
+ // Pattern A (JS/C#/Rust): if_statement inside else_clause
202
+ isElseIf = node.parent?.type === rules.elseNodeType;
203
+ }
204
+ if (isElseIf) {
205
+ acc.cognitive++;
206
+ acc.cyclomatic++;
207
+ walkChildren(node, nestingLevel, walkFn);
208
+ return true;
209
+ }
210
+ }
211
+
212
+ return false;
213
+ }
214
+
215
+ /** Handle branch/control flow nodes. Returns true if handled. */
216
+ function handleBranchNode(
217
+ node: TreeSitterNode,
218
+ type: string,
219
+ rules: ComplexityRules,
220
+ acc: ComplexityAccumulator,
221
+ nestingLevel: number,
222
+ walkFn: WalkFn,
223
+ ): boolean {
224
+ if (!rules.branchNodes.has(type) || node.childCount === 0) return false;
225
+
226
+ if (handleElseClause(node, type, rules, acc, nestingLevel, walkFn)) return true;
227
+ if (handleElseIf(node, type, rules, acc, nestingLevel, walkFn)) return true;
228
+
229
+ // Regular branch node
230
+ acc.cognitive += 1 + nestingLevel; // structural + nesting
231
+ acc.cyclomatic++;
232
+
233
+ // Switch-like nodes don't add cyclomatic themselves (cases do)
234
+ if (rules.switchLikeNodes?.has(type)) {
235
+ acc.cyclomatic--;
236
+ }
237
+
238
+ if (rules.nestingNodes.has(type)) {
239
+ walkChildren(node, nestingLevel + 1, walkFn);
240
+ return true;
241
+ }
242
+
243
+ return false;
244
+ }
245
+
246
+ /** Handle Pattern C plain else: block is the alternative of an if_statement (Go/Java). */
247
+ function handlePatternCElse(
248
+ node: TreeSitterNode,
249
+ type: string,
250
+ rules: ComplexityRules,
251
+ acc: ComplexityAccumulator,
252
+ nestingLevel: number,
253
+ walkFn: WalkFn,
254
+ ): boolean {
255
+ if (
256
+ !rules.elseViaAlternative ||
257
+ type === rules.ifNodeType ||
258
+ node.parent?.type !== rules.ifNodeType ||
259
+ node.parent.childForFieldName('alternative')?.id !== node.id
260
+ ) {
261
+ return false;
262
+ }
263
+
264
+ acc.cognitive++;
265
+ walkChildren(node, nestingLevel, walkFn);
266
+ return true;
267
+ }
268
+
112
269
  export function computeFunctionComplexity(
113
270
  functionNode: TreeSitterNode,
114
271
  language: string,
@@ -116,158 +273,38 @@ export function computeFunctionComplexity(
116
273
  const rules = COMPLEXITY_RULES.get(language) as ComplexityRules | undefined;
117
274
  if (!rules) return null;
118
275
 
119
- let cognitive = 0;
120
- let cyclomatic = 1; // McCabe starts at 1
121
- let maxNesting = 0;
276
+ const acc: ComplexityAccumulator = { cognitive: 0, cyclomatic: 1, maxNesting: 0 };
122
277
 
123
278
  function walk(node: TreeSitterNode | null, nestingLevel: number, isTopFunction: boolean): void {
124
279
  if (!node) return;
125
280
 
126
281
  const type = node.type;
127
282
 
128
- // Track nesting depth
129
- if (nestingLevel > maxNesting) maxNesting = nestingLevel;
130
-
131
- // Handle logical operators in binary expressions
132
- if (type === rules?.logicalNodeType) {
133
- const op = node.child(1)?.type;
134
- if (op && rules?.logicalOperators.has(op)) {
135
- // Cyclomatic: +1 for every logical operator
136
- cyclomatic++;
137
-
138
- // Cognitive: +1 only when operator changes from the previous sibling sequence
139
- // Walk up to check if parent is same type with same operator
140
- const parent = node.parent;
141
- let sameSequence = false;
142
- if (parent && parent.type === rules?.logicalNodeType) {
143
- const parentOp = parent.child(1)?.type;
144
- if (parentOp === op) {
145
- sameSequence = true;
146
- }
147
- }
148
- if (!sameSequence) {
149
- cognitive++;
150
- }
151
-
152
- // Walk children manually to avoid double-counting
153
- for (let i = 0; i < node.childCount; i++) {
154
- walk(node.child(i), nestingLevel, false);
155
- }
156
- return;
157
- }
158
- }
283
+ if (nestingLevel > acc.maxNesting) acc.maxNesting = nestingLevel;
159
284
 
160
- // Handle optional chaining (cyclomatic only)
161
- if (type === rules?.optionalChainType) {
162
- cyclomatic++;
163
- }
164
-
165
- // Handle branch/control flow nodes (skip keyword leaf tokens like Ruby's `if`)
166
- if (rules?.branchNodes.has(type) && node.childCount > 0) {
167
- // Pattern A: else clause wraps if (JS/C#/Rust)
168
- if (rules?.elseNodeType && type === rules?.elseNodeType) {
169
- const firstChild = node.namedChild(0);
170
- if (firstChild && firstChild.type === rules?.ifNodeType) {
171
- // else-if: the if_statement child handles its own increment
172
- for (let i = 0; i < node.childCount; i++) {
173
- walk(node.child(i), nestingLevel, false);
174
- }
175
- return;
176
- }
177
- // Plain else
178
- cognitive++;
179
- for (let i = 0; i < node.childCount; i++) {
180
- walk(node.child(i), nestingLevel, false);
181
- }
182
- return;
183
- }
285
+ if (handleLogicalOperator(node, type, rules!, acc, nestingLevel, walk)) return;
184
286
 
185
- // Pattern B: explicit elif node (Python/Ruby/PHP)
186
- if (rules?.elifNodeType && type === rules?.elifNodeType) {
187
- cognitive++;
188
- cyclomatic++;
189
- for (let i = 0; i < node.childCount; i++) {
190
- walk(node.child(i), nestingLevel, false);
191
- }
192
- return;
193
- }
287
+ // Optional chaining (cyclomatic only)
288
+ if (type === rules!.optionalChainType) acc.cyclomatic++;
194
289
 
195
- // Detect else-if via Pattern A or C
196
- let isElseIf = false;
197
- if (type === rules?.ifNodeType) {
198
- if (rules?.elseViaAlternative) {
199
- // Pattern C (Go/Java): if_statement is the alternative of parent if_statement
200
- isElseIf =
201
- node.parent?.type === rules?.ifNodeType &&
202
- node.parent.childForFieldName('alternative')?.id === node.id;
203
- } else if (rules?.elseNodeType) {
204
- // Pattern A (JS/C#/Rust): if_statement inside else_clause
205
- isElseIf = node.parent?.type === rules?.elseNodeType;
206
- }
207
- }
290
+ if (handleBranchNode(node, type, rules!, acc, nestingLevel, walk)) return;
291
+ if (handlePatternCElse(node, type, rules!, acc, nestingLevel, walk)) return;
208
292
 
209
- if (isElseIf) {
210
- cognitive++;
211
- cyclomatic++;
212
- for (let i = 0; i < node.childCount; i++) {
213
- walk(node.child(i), nestingLevel, false);
214
- }
215
- return;
216
- }
293
+ // Case nodes (cyclomatic only, skip keyword leaves)
294
+ if (rules!.caseNodes.has(type) && node.childCount > 0) acc.cyclomatic++;
217
295
 
218
- // Regular branch node
219
- cognitive += 1 + nestingLevel; // structural + nesting
220
- cyclomatic++;
221
-
222
- // Switch-like nodes don't add cyclomatic themselves (cases do)
223
- if (rules?.switchLikeNodes?.has(type)) {
224
- cyclomatic--; // Undo the ++ above; cases handle cyclomatic
225
- }
226
-
227
- if (rules?.nestingNodes.has(type)) {
228
- for (let i = 0; i < node.childCount; i++) {
229
- walk(node.child(i), nestingLevel + 1, false);
230
- }
231
- return;
232
- }
233
- }
234
-
235
- // Pattern C plain else: block that is the alternative of an if_statement (Go/Java)
236
- if (
237
- rules?.elseViaAlternative &&
238
- type !== rules?.ifNodeType &&
239
- node.parent?.type === rules?.ifNodeType &&
240
- node.parent.childForFieldName('alternative')?.id === node.id
241
- ) {
242
- cognitive++;
243
- for (let i = 0; i < node.childCount; i++) {
244
- walk(node.child(i), nestingLevel, false);
245
- }
296
+ // Nested function definitions (increase nesting)
297
+ if (!isTopFunction && rules!.functionNodes.has(type)) {
298
+ walkChildren(node, nestingLevel + 1, walk);
246
299
  return;
247
300
  }
248
301
 
249
- // Handle case nodes (cyclomatic only, skip keyword leaves)
250
- if (rules?.caseNodes.has(type) && node.childCount > 0) {
251
- cyclomatic++;
252
- }
253
-
254
- // Handle nested function definitions (increase nesting)
255
- if (!isTopFunction && rules?.functionNodes.has(type)) {
256
- for (let i = 0; i < node.childCount; i++) {
257
- walk(node.child(i), nestingLevel + 1, false);
258
- }
259
- return;
260
- }
261
-
262
- // Walk children
263
- for (let i = 0; i < node.childCount; i++) {
264
- walk(node.child(i), nestingLevel, false);
265
- }
302
+ walkChildren(node, nestingLevel, walk);
266
303
  }
267
304
 
268
305
  walk(functionNode, 0, true);
269
306
 
270
- return { cognitive, cyclomatic, maxNesting };
307
+ return { cognitive: acc.cognitive, cyclomatic: acc.cyclomatic, maxNesting: acc.maxNesting };
271
308
  }
272
309
 
273
310
  // ─── Merged Single-Pass Computation ───────────────────────────────────────