@optave/codegraph 3.10.0 → 3.11.1

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 (312) hide show
  1. package/README.md +40 -33
  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/rules/index.d.ts.map +1 -1
  6. package/dist/ast-analysis/rules/index.js +77 -0
  7. package/dist/ast-analysis/rules/index.js.map +1 -1
  8. package/dist/ast-analysis/visitor-utils.d.ts +3 -0
  9. package/dist/ast-analysis/visitor-utils.d.ts.map +1 -1
  10. package/dist/ast-analysis/visitor-utils.js +83 -49
  11. package/dist/ast-analysis/visitor-utils.js.map +1 -1
  12. package/dist/ast-analysis/visitors/ast-store-visitor.d.ts.map +1 -1
  13. package/dist/ast-analysis/visitors/ast-store-visitor.js +78 -62
  14. package/dist/ast-analysis/visitors/ast-store-visitor.js.map +1 -1
  15. package/dist/ast-analysis/visitors/dataflow-visitor.d.ts.map +1 -1
  16. package/dist/ast-analysis/visitors/dataflow-visitor.js +61 -42
  17. package/dist/ast-analysis/visitors/dataflow-visitor.js.map +1 -1
  18. package/dist/cli/commands/audit.js +1 -1
  19. package/dist/cli/commands/audit.js.map +1 -1
  20. package/dist/cli/commands/build.d.ts.map +1 -1
  21. package/dist/cli/commands/build.js +2 -0
  22. package/dist/cli/commands/build.js.map +1 -1
  23. package/dist/cli/commands/check.js +1 -1
  24. package/dist/cli/commands/check.js.map +1 -1
  25. package/dist/cli/commands/children.js +1 -1
  26. package/dist/cli/commands/children.js.map +1 -1
  27. package/dist/cli/commands/diff-impact.js +1 -1
  28. package/dist/cli/commands/diff-impact.js.map +1 -1
  29. package/dist/cli/commands/embed.d.ts.map +1 -1
  30. package/dist/cli/commands/embed.js +49 -4
  31. package/dist/cli/commands/embed.js.map +1 -1
  32. package/dist/cli/commands/roles.js +1 -1
  33. package/dist/cli/commands/roles.js.map +1 -1
  34. package/dist/cli/commands/structure.js +1 -1
  35. package/dist/cli/commands/structure.js.map +1 -1
  36. package/dist/cli/shared/options.js +1 -1
  37. package/dist/cli/shared/options.js.map +1 -1
  38. package/dist/db/connection.d.ts.map +1 -1
  39. package/dist/db/connection.js +8 -0
  40. package/dist/db/connection.js.map +1 -1
  41. package/dist/domain/analysis/dependencies.d.ts.map +1 -1
  42. package/dist/domain/analysis/dependencies.js +106 -80
  43. package/dist/domain/analysis/dependencies.js.map +1 -1
  44. package/dist/domain/analysis/fn-impact.d.ts.map +1 -1
  45. package/dist/domain/analysis/fn-impact.js +77 -52
  46. package/dist/domain/analysis/fn-impact.js.map +1 -1
  47. package/dist/domain/analysis/module-map.d.ts.map +1 -1
  48. package/dist/domain/analysis/module-map.js +132 -121
  49. package/dist/domain/analysis/module-map.js.map +1 -1
  50. package/dist/domain/graph/builder/helpers.d.ts +4 -4
  51. package/dist/domain/graph/builder/helpers.d.ts.map +1 -1
  52. package/dist/domain/graph/builder/helpers.js +47 -33
  53. package/dist/domain/graph/builder/helpers.js.map +1 -1
  54. package/dist/domain/graph/builder/incremental.d.ts +6 -6
  55. package/dist/domain/graph/builder/incremental.d.ts.map +1 -1
  56. package/dist/domain/graph/builder/incremental.js +148 -99
  57. package/dist/domain/graph/builder/incremental.js.map +1 -1
  58. package/dist/domain/graph/builder/pipeline.d.ts +1 -0
  59. package/dist/domain/graph/builder/pipeline.d.ts.map +1 -1
  60. package/dist/domain/graph/builder/pipeline.js +23 -637
  61. package/dist/domain/graph/builder/pipeline.js.map +1 -1
  62. package/dist/domain/graph/builder/stages/build-edges.d.ts.map +1 -1
  63. package/dist/domain/graph/builder/stages/build-edges.js +141 -98
  64. package/dist/domain/graph/builder/stages/build-edges.js.map +1 -1
  65. package/dist/domain/graph/builder/stages/build-structure.d.ts.map +1 -1
  66. package/dist/domain/graph/builder/stages/build-structure.js +82 -65
  67. package/dist/domain/graph/builder/stages/build-structure.js.map +1 -1
  68. package/dist/domain/graph/builder/stages/detect-changes.d.ts.map +1 -1
  69. package/dist/domain/graph/builder/stages/detect-changes.js +84 -56
  70. package/dist/domain/graph/builder/stages/detect-changes.js.map +1 -1
  71. package/dist/domain/graph/builder/stages/finalize.d.ts.map +1 -1
  72. package/dist/domain/graph/builder/stages/finalize.js +60 -51
  73. package/dist/domain/graph/builder/stages/finalize.js.map +1 -1
  74. package/dist/domain/graph/builder/stages/insert-nodes.d.ts +8 -6
  75. package/dist/domain/graph/builder/stages/insert-nodes.d.ts.map +1 -1
  76. package/dist/domain/graph/builder/stages/insert-nodes.js +107 -122
  77. package/dist/domain/graph/builder/stages/insert-nodes.js.map +1 -1
  78. package/dist/domain/graph/builder/stages/native-db-lifecycle.d.ts +14 -0
  79. package/dist/domain/graph/builder/stages/native-db-lifecycle.d.ts.map +1 -0
  80. package/dist/domain/graph/builder/stages/native-db-lifecycle.js +77 -0
  81. package/dist/domain/graph/builder/stages/native-db-lifecycle.js.map +1 -0
  82. package/dist/domain/graph/builder/stages/native-orchestrator.d.ts +62 -0
  83. package/dist/domain/graph/builder/stages/native-orchestrator.d.ts.map +1 -0
  84. package/dist/domain/graph/builder/stages/native-orchestrator.js +747 -0
  85. package/dist/domain/graph/builder/stages/native-orchestrator.js.map +1 -0
  86. package/dist/domain/graph/builder/stages/resolve-imports.d.ts.map +1 -1
  87. package/dist/domain/graph/builder/stages/resolve-imports.js +73 -22
  88. package/dist/domain/graph/builder/stages/resolve-imports.js.map +1 -1
  89. package/dist/domain/graph/cycles.d.ts +6 -4
  90. package/dist/domain/graph/cycles.d.ts.map +1 -1
  91. package/dist/domain/graph/cycles.js +50 -55
  92. package/dist/domain/graph/cycles.js.map +1 -1
  93. package/dist/domain/graph/journal.d.ts.map +1 -1
  94. package/dist/domain/graph/journal.js +89 -70
  95. package/dist/domain/graph/journal.js.map +1 -1
  96. package/dist/domain/graph/watcher.d.ts.map +1 -1
  97. package/dist/domain/graph/watcher.js +28 -20
  98. package/dist/domain/graph/watcher.js.map +1 -1
  99. package/dist/domain/parser.d.ts +12 -23
  100. package/dist/domain/parser.d.ts.map +1 -1
  101. package/dist/domain/parser.js +153 -80
  102. package/dist/domain/parser.js.map +1 -1
  103. package/dist/domain/search/generator.d.ts +3 -1
  104. package/dist/domain/search/generator.d.ts.map +1 -1
  105. package/dist/domain/search/generator.js +68 -45
  106. package/dist/domain/search/generator.js.map +1 -1
  107. package/dist/domain/search/models.d.ts +18 -0
  108. package/dist/domain/search/models.d.ts.map +1 -1
  109. package/dist/domain/search/models.js +72 -4
  110. package/dist/domain/search/models.js.map +1 -1
  111. package/dist/domain/search/search/hybrid.d.ts.map +1 -1
  112. package/dist/domain/search/search/hybrid.js +49 -40
  113. package/dist/domain/search/search/hybrid.js.map +1 -1
  114. package/dist/domain/search/search/semantic.d.ts.map +1 -1
  115. package/dist/domain/search/search/semantic.js +69 -49
  116. package/dist/domain/search/search/semantic.js.map +1 -1
  117. package/dist/domain/wasm-worker-entry.js +209 -137
  118. package/dist/domain/wasm-worker-entry.js.map +1 -1
  119. package/dist/extractors/c.js +25 -6
  120. package/dist/extractors/c.js.map +1 -1
  121. package/dist/extractors/cpp.js +47 -6
  122. package/dist/extractors/cpp.js.map +1 -1
  123. package/dist/extractors/cuda.js +90 -14
  124. package/dist/extractors/cuda.js.map +1 -1
  125. package/dist/extractors/elixir.js +108 -4
  126. package/dist/extractors/elixir.js.map +1 -1
  127. package/dist/extractors/erlang.js +56 -20
  128. package/dist/extractors/erlang.js.map +1 -1
  129. package/dist/extractors/fsharp.d.ts +7 -0
  130. package/dist/extractors/fsharp.d.ts.map +1 -1
  131. package/dist/extractors/fsharp.js +94 -0
  132. package/dist/extractors/fsharp.js.map +1 -1
  133. package/dist/extractors/gleam.d.ts.map +1 -1
  134. package/dist/extractors/gleam.js +29 -33
  135. package/dist/extractors/gleam.js.map +1 -1
  136. package/dist/extractors/groovy.js +41 -1
  137. package/dist/extractors/groovy.js.map +1 -1
  138. package/dist/extractors/haskell.js +48 -4
  139. package/dist/extractors/haskell.js.map +1 -1
  140. package/dist/extractors/helpers.d.ts +79 -1
  141. package/dist/extractors/helpers.d.ts.map +1 -1
  142. package/dist/extractors/helpers.js +137 -0
  143. package/dist/extractors/helpers.js.map +1 -1
  144. package/dist/extractors/java.d.ts.map +1 -1
  145. package/dist/extractors/java.js +37 -49
  146. package/dist/extractors/java.js.map +1 -1
  147. package/dist/extractors/javascript.d.ts.map +1 -1
  148. package/dist/extractors/javascript.js +44 -44
  149. package/dist/extractors/javascript.js.map +1 -1
  150. package/dist/extractors/julia.js +198 -74
  151. package/dist/extractors/julia.js.map +1 -1
  152. package/dist/extractors/kotlin.js +4 -0
  153. package/dist/extractors/kotlin.js.map +1 -1
  154. package/dist/extractors/objc.js +184 -47
  155. package/dist/extractors/objc.js.map +1 -1
  156. package/dist/extractors/python.js +7 -4
  157. package/dist/extractors/python.js.map +1 -1
  158. package/dist/extractors/r.d.ts.map +1 -1
  159. package/dist/extractors/r.js +103 -87
  160. package/dist/extractors/r.js.map +1 -1
  161. package/dist/extractors/scala.d.ts.map +1 -1
  162. package/dist/extractors/scala.js +18 -32
  163. package/dist/extractors/scala.js.map +1 -1
  164. package/dist/extractors/solidity.d.ts.map +1 -1
  165. package/dist/extractors/solidity.js +55 -69
  166. package/dist/extractors/solidity.js.map +1 -1
  167. package/dist/extractors/verilog.js +80 -15
  168. package/dist/extractors/verilog.js.map +1 -1
  169. package/dist/features/boundaries.d.ts.map +1 -1
  170. package/dist/features/boundaries.js +49 -39
  171. package/dist/features/boundaries.js.map +1 -1
  172. package/dist/features/cfg.d.ts.map +1 -1
  173. package/dist/features/cfg.js +90 -63
  174. package/dist/features/cfg.js.map +1 -1
  175. package/dist/features/check.d.ts.map +1 -1
  176. package/dist/features/check.js +43 -34
  177. package/dist/features/check.js.map +1 -1
  178. package/dist/features/cochange.d.ts.map +1 -1
  179. package/dist/features/cochange.js +68 -56
  180. package/dist/features/cochange.js.map +1 -1
  181. package/dist/features/complexity.d.ts.map +1 -1
  182. package/dist/features/complexity.js +105 -75
  183. package/dist/features/complexity.js.map +1 -1
  184. package/dist/features/dataflow.d.ts.map +1 -1
  185. package/dist/features/dataflow.js +37 -29
  186. package/dist/features/dataflow.js.map +1 -1
  187. package/dist/features/flow.d.ts.map +1 -1
  188. package/dist/features/flow.js +31 -22
  189. package/dist/features/flow.js.map +1 -1
  190. package/dist/features/graph-enrichment.d.ts.map +1 -1
  191. package/dist/features/graph-enrichment.js +77 -70
  192. package/dist/features/graph-enrichment.js.map +1 -1
  193. package/dist/features/owners.d.ts +17 -26
  194. package/dist/features/owners.d.ts.map +1 -1
  195. package/dist/features/owners.js +120 -109
  196. package/dist/features/owners.js.map +1 -1
  197. package/dist/features/sequence.d.ts.map +1 -1
  198. package/dist/features/sequence.js +59 -54
  199. package/dist/features/sequence.js.map +1 -1
  200. package/dist/features/structure-query.d.ts.map +1 -1
  201. package/dist/features/structure-query.js +60 -60
  202. package/dist/features/structure-query.js.map +1 -1
  203. package/dist/features/structure.js +28 -36
  204. package/dist/features/structure.js.map +1 -1
  205. package/dist/graph/algorithms/leiden/optimiser.d.ts.map +1 -1
  206. package/dist/graph/algorithms/leiden/optimiser.js +100 -69
  207. package/dist/graph/algorithms/leiden/optimiser.js.map +1 -1
  208. package/dist/graph/classifiers/roles.d.ts.map +1 -1
  209. package/dist/graph/classifiers/roles.js +63 -59
  210. package/dist/graph/classifiers/roles.js.map +1 -1
  211. package/dist/infrastructure/config.d.ts +1 -1
  212. package/dist/infrastructure/config.d.ts.map +1 -1
  213. package/dist/infrastructure/config.js +1 -1
  214. package/dist/infrastructure/config.js.map +1 -1
  215. package/dist/mcp/tool-registry.d.ts.map +1 -1
  216. package/dist/mcp/tool-registry.js +4 -0
  217. package/dist/mcp/tool-registry.js.map +1 -1
  218. package/dist/mcp/tools/semantic-search.d.ts +1 -0
  219. package/dist/mcp/tools/semantic-search.d.ts.map +1 -1
  220. package/dist/mcp/tools/semantic-search.js +1 -0
  221. package/dist/mcp/tools/semantic-search.js.map +1 -1
  222. package/dist/presentation/cfg.d.ts.map +1 -1
  223. package/dist/presentation/cfg.js +44 -29
  224. package/dist/presentation/cfg.js.map +1 -1
  225. package/dist/presentation/flow.d.ts.map +1 -1
  226. package/dist/presentation/flow.js +58 -38
  227. package/dist/presentation/flow.js.map +1 -1
  228. package/dist/types.d.ts +16 -2
  229. package/dist/types.d.ts.map +1 -1
  230. package/grammars/tree-sitter-erlang.wasm +0 -0
  231. package/grammars/tree-sitter-fsharp.wasm +0 -0
  232. package/grammars/tree-sitter-fsharp_signature.wasm +0 -0
  233. package/grammars/tree-sitter-gleam.wasm +0 -0
  234. package/package.json +10 -10
  235. package/src/ast-analysis/engine.ts +145 -61
  236. package/src/ast-analysis/rules/index.ts +87 -0
  237. package/src/ast-analysis/visitor-utils.ts +86 -46
  238. package/src/ast-analysis/visitors/ast-store-visitor.ts +104 -69
  239. package/src/ast-analysis/visitors/dataflow-visitor.ts +86 -47
  240. package/src/cli/commands/audit.ts +1 -1
  241. package/src/cli/commands/build.ts +2 -0
  242. package/src/cli/commands/check.ts +1 -1
  243. package/src/cli/commands/children.ts +1 -1
  244. package/src/cli/commands/diff-impact.ts +1 -1
  245. package/src/cli/commands/embed.ts +54 -4
  246. package/src/cli/commands/roles.ts +1 -1
  247. package/src/cli/commands/structure.ts +1 -1
  248. package/src/cli/shared/options.ts +1 -1
  249. package/src/db/connection.ts +8 -0
  250. package/src/domain/analysis/dependencies.ts +166 -85
  251. package/src/domain/analysis/fn-impact.ts +120 -50
  252. package/src/domain/analysis/module-map.ts +175 -140
  253. package/src/domain/graph/builder/helpers.ts +85 -76
  254. package/src/domain/graph/builder/incremental.ts +223 -131
  255. package/src/domain/graph/builder/pipeline.ts +32 -785
  256. package/src/domain/graph/builder/stages/build-edges.ts +207 -142
  257. package/src/domain/graph/builder/stages/build-structure.ts +115 -82
  258. package/src/domain/graph/builder/stages/detect-changes.ts +107 -64
  259. package/src/domain/graph/builder/stages/finalize.ts +72 -70
  260. package/src/domain/graph/builder/stages/insert-nodes.ts +154 -120
  261. package/src/domain/graph/builder/stages/native-db-lifecycle.ts +74 -0
  262. package/src/domain/graph/builder/stages/native-orchestrator.ts +942 -0
  263. package/src/domain/graph/builder/stages/resolve-imports.ts +79 -25
  264. package/src/domain/graph/cycles.ts +51 -49
  265. package/src/domain/graph/journal.ts +84 -69
  266. package/src/domain/graph/watcher.ts +29 -25
  267. package/src/domain/parser.ts +170 -67
  268. package/src/domain/search/generator.ts +132 -74
  269. package/src/domain/search/models.ts +75 -4
  270. package/src/domain/search/search/hybrid.ts +53 -42
  271. package/src/domain/search/search/semantic.ts +105 -65
  272. package/src/domain/wasm-worker-entry.ts +243 -153
  273. package/src/extractors/c.ts +27 -8
  274. package/src/extractors/cpp.ts +50 -8
  275. package/src/extractors/cuda.ts +90 -16
  276. package/src/extractors/elixir.ts +103 -4
  277. package/src/extractors/erlang.ts +63 -20
  278. package/src/extractors/fsharp.ts +104 -0
  279. package/src/extractors/gleam.ts +40 -39
  280. package/src/extractors/groovy.ts +45 -1
  281. package/src/extractors/haskell.ts +45 -4
  282. package/src/extractors/helpers.ts +205 -1
  283. package/src/extractors/java.ts +42 -45
  284. package/src/extractors/javascript.ts +44 -43
  285. package/src/extractors/julia.ts +191 -77
  286. package/src/extractors/kotlin.ts +4 -0
  287. package/src/extractors/objc.ts +171 -47
  288. package/src/extractors/python.ts +5 -3
  289. package/src/extractors/r.ts +104 -82
  290. package/src/extractors/scala.ts +24 -36
  291. package/src/extractors/solidity.ts +59 -78
  292. package/src/extractors/verilog.ts +83 -15
  293. package/src/features/boundaries.ts +64 -46
  294. package/src/features/cfg.ts +145 -74
  295. package/src/features/check.ts +60 -43
  296. package/src/features/cochange.ts +95 -72
  297. package/src/features/complexity.ts +134 -79
  298. package/src/features/dataflow.ts +57 -34
  299. package/src/features/flow.ts +48 -24
  300. package/src/features/graph-enrichment.ts +105 -70
  301. package/src/features/owners.ts +186 -146
  302. package/src/features/sequence.ts +99 -69
  303. package/src/features/structure-query.ts +94 -79
  304. package/src/features/structure.ts +56 -56
  305. package/src/graph/algorithms/leiden/optimiser.ts +142 -87
  306. package/src/graph/classifiers/roles.ts +64 -54
  307. package/src/infrastructure/config.ts +1 -1
  308. package/src/mcp/tool-registry.ts +5 -0
  309. package/src/mcp/tools/semantic-search.ts +2 -0
  310. package/src/presentation/cfg.ts +48 -32
  311. package/src/presentation/flow.ts +100 -52
  312. package/src/types.ts +16 -1
@@ -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) ──────────────────────────────────
@@ -139,18 +139,25 @@ interface OwnersDataOpts {
139
139
  boundary?: boolean;
140
140
  }
141
141
 
142
- export function ownersData(
143
- customDbPath?: string,
144
- opts: OwnersDataOpts = {},
145
- ): {
142
+ interface OwnedSymbol {
143
+ name: string;
144
+ kind: string;
145
+ file: string;
146
+ line: number;
147
+ owners: string[];
148
+ }
149
+
150
+ interface OwnerBoundary {
151
+ from: OwnedSymbol;
152
+ to: OwnedSymbol;
153
+ edgeKind: string;
154
+ }
155
+
156
+ interface OwnersDataResult {
146
157
  codeownersFile: string | null;
147
158
  files: { file: string; owners: string[] }[];
148
- symbols: { name: string; kind: string; file: string; line: number; owners: string[] }[];
149
- boundaries: {
150
- from: { name: string; kind: string; file: string; line: number; owners: string[] };
151
- to: { name: string; kind: string; file: string; line: number; owners: string[] };
152
- edgeKind: string;
153
- }[];
159
+ symbols: OwnedSymbol[];
160
+ boundaries: OwnerBoundary[];
154
161
  summary: {
155
162
  totalFiles: number;
156
163
  ownedFiles: number;
@@ -159,160 +166,193 @@ export function ownersData(
159
166
  ownerCount: number;
160
167
  byOwner: { owner: string; fileCount: number }[];
161
168
  };
169
+ }
170
+
171
+ interface BetterSqlite3DatabaseLike {
172
+ prepare(sql: string): { all(...params: unknown[]): unknown[] };
173
+ close(): void;
174
+ }
175
+
176
+ function emptyOwnersResult(codeownersFile: string | null): OwnersDataResult {
177
+ return {
178
+ codeownersFile,
179
+ files: [],
180
+ symbols: [],
181
+ boundaries: [],
182
+ summary: {
183
+ totalFiles: 0,
184
+ ownedFiles: 0,
185
+ unownedFiles: 0,
186
+ coveragePercent: 0,
187
+ ownerCount: 0,
188
+ byOwner: [],
189
+ },
190
+ };
191
+ }
192
+
193
+ /** Load all distinct files from the DB and apply test/file filters. */
194
+ function loadFilteredFiles(db: BetterSqlite3DatabaseLike, opts: OwnersDataOpts): string[] {
195
+ let allFiles = (db.prepare('SELECT DISTINCT file FROM nodes').all() as { file: string }[]).map(
196
+ (r) => r.file,
197
+ );
198
+ if (opts.noTests) allFiles = allFiles.filter((f) => !isTestFile(f));
199
+ const fileFilters = normalizeFileFilter(opts.file);
200
+ if (fileFilters.length > 0) {
201
+ allFiles = allFiles.filter((f) => fileFilters.some((filter) => f.includes(filter)));
202
+ }
203
+ return allFiles;
204
+ }
205
+
206
+ /** Build owner index (owner -> list of files) and count owned files. */
207
+ function buildOwnerIndex(fileOwners: { file: string; owners: string[] }[]): {
208
+ ownerIndex: Map<string, string[]>;
209
+ ownedCount: number;
162
210
  } {
211
+ const ownerIndex = new Map<string, string[]>();
212
+ let ownedCount = 0;
213
+ for (const fo of fileOwners) {
214
+ if (fo.owners.length > 0) ownedCount++;
215
+ for (const o of fo.owners) {
216
+ if (!ownerIndex.has(o)) ownerIndex.set(o, []);
217
+ ownerIndex.get(o)!.push(fo.file);
218
+ }
219
+ }
220
+ return { ownerIndex, ownedCount };
221
+ }
222
+
223
+ /** Load symbols restricted to the given file set, applying noTests and kind filters. */
224
+ function loadSymbolsForFiles(
225
+ db: BetterSqlite3DatabaseLike,
226
+ fileSet: Set<string>,
227
+ opts: OwnersDataOpts,
228
+ rules: CodeownersRule[],
229
+ ): OwnedSymbol[] {
230
+ let symbols = (
231
+ db.prepare('SELECT name, kind, file, line FROM nodes').all() as {
232
+ name: string;
233
+ kind: string;
234
+ file: string;
235
+ line: number;
236
+ }[]
237
+ ).filter((n) => fileSet.has(n.file));
238
+
239
+ if (opts.noTests) symbols = symbols.filter((s) => !isTestFile(s.file));
240
+ if (opts.kind) symbols = symbols.filter((s) => s.kind === opts.kind);
241
+
242
+ return symbols.map((s) => ({ ...s, owners: matchOwners(s.file, rules) }));
243
+ }
244
+
245
+ interface CallEdgeRow {
246
+ id: number;
247
+ edgeKind: string;
248
+ srcName: string;
249
+ srcKind: string;
250
+ srcFile: string;
251
+ srcLine: number;
252
+ tgtName: string;
253
+ tgtKind: string;
254
+ tgtFile: string;
255
+ tgtLine: number;
256
+ }
257
+
258
+ /** Compute cross-owner call boundaries. Returns empty array when boundary mode is off. */
259
+ function computeOwnerBoundaries(
260
+ db: BetterSqlite3DatabaseLike,
261
+ rules: CodeownersRule[],
262
+ noTests: boolean,
263
+ ): OwnerBoundary[] {
264
+ const edges = db
265
+ .prepare(
266
+ `SELECT e.id, e.kind AS edgeKind,
267
+ s.name AS srcName, s.kind AS srcKind, s.file AS srcFile, s.line AS srcLine,
268
+ t.name AS tgtName, t.kind AS tgtKind, t.file AS tgtFile, t.line AS tgtLine
269
+ FROM edges e
270
+ JOIN nodes s ON e.source_id = s.id
271
+ JOIN nodes t ON e.target_id = t.id
272
+ WHERE e.kind = 'calls'`,
273
+ )
274
+ .all() as CallEdgeRow[];
275
+
276
+ const boundaries: OwnerBoundary[] = [];
277
+ for (const e of edges) {
278
+ if (noTests && (isTestFile(e.srcFile) || isTestFile(e.tgtFile))) continue;
279
+ const srcOwners = matchOwners(e.srcFile, rules);
280
+ const tgtOwners = matchOwners(e.tgtFile, rules);
281
+ // Cross-boundary: different owner sets (sort for deterministic comparison + output)
282
+ const sortedSrc = [...srcOwners].sort();
283
+ const sortedTgt = [...tgtOwners].sort();
284
+ const srcKey = sortedSrc.join(',');
285
+ const tgtKey = sortedTgt.join(',');
286
+ if (srcKey === tgtKey) continue;
287
+ boundaries.push({
288
+ from: {
289
+ name: e.srcName,
290
+ kind: e.srcKind,
291
+ file: e.srcFile,
292
+ line: e.srcLine,
293
+ owners: sortedSrc,
294
+ },
295
+ to: { name: e.tgtName, kind: e.tgtKind, file: e.tgtFile, line: e.tgtLine, owners: sortedTgt },
296
+ edgeKind: e.edgeKind,
297
+ });
298
+ }
299
+ return boundaries;
300
+ }
301
+
302
+ /** Build summary stats (totals, coverage, by-owner counts). */
303
+ function buildOwnersSummary(
304
+ totalFiles: number,
305
+ ownedCount: number,
306
+ ownerIndex: Map<string, string[]>,
307
+ ): OwnersDataResult['summary'] {
308
+ const byOwner = [...ownerIndex.entries()]
309
+ .map(([owner, files]) => ({ owner, fileCount: files.length }))
310
+ .sort((a, b) => b.fileCount - a.fileCount);
311
+
312
+ return {
313
+ totalFiles,
314
+ ownedFiles: ownedCount,
315
+ unownedFiles: totalFiles - ownedCount,
316
+ coveragePercent: totalFiles > 0 ? Math.round((ownedCount / totalFiles) * 100) : 0,
317
+ ownerCount: ownerIndex.size,
318
+ byOwner,
319
+ };
320
+ }
321
+
322
+ export function ownersData(customDbPath?: string, opts: OwnersDataOpts = {}): OwnersDataResult {
163
323
  const db = openReadonlyOrFail(customDbPath);
164
324
  try {
165
325
  const dbPath = findDbPath(customDbPath);
166
326
  const repoRoot = path.resolve(path.dirname(dbPath), '..');
167
327
 
168
328
  const parsed = parseCodeowners(repoRoot);
169
- if (!parsed) {
170
- return {
171
- codeownersFile: null,
172
- files: [],
173
- symbols: [],
174
- boundaries: [],
175
- summary: {
176
- totalFiles: 0,
177
- ownedFiles: 0,
178
- unownedFiles: 0,
179
- coveragePercent: 0,
180
- ownerCount: 0,
181
- byOwner: [],
182
- },
183
- };
184
- }
185
-
186
- // Get all distinct files from nodes
187
- let allFiles = (db.prepare('SELECT DISTINCT file FROM nodes').all() as { file: string }[]).map(
188
- (r) => r.file,
189
- );
329
+ if (!parsed) return emptyOwnersResult(null);
190
330
 
191
- if (opts.noTests) allFiles = allFiles.filter((f) => !isTestFile(f));
192
- const fileFilters = normalizeFileFilter(opts.file);
193
- if (fileFilters.length > 0) {
194
- allFiles = allFiles.filter((f) => fileFilters.some((filter) => f.includes(filter)));
195
- }
196
-
197
- // Map files to owners
198
- const fileOwners = allFiles.map((file) => ({
199
- file,
200
- owners: matchOwners(file, parsed.rules),
201
- }));
202
-
203
- // Build owner-to-files index
204
- const ownerIndex = new Map<string, string[]>();
205
- let ownedCount = 0;
206
- for (const fo of fileOwners) {
207
- if (fo.owners.length > 0) ownedCount++;
208
- for (const o of fo.owners) {
209
- if (!ownerIndex.has(o)) ownerIndex.set(o, []);
210
- ownerIndex.get(o)!.push(fo.file);
211
- }
212
- }
331
+ // Stage 1: load files and bucket them by owner
332
+ const allFiles = loadFilteredFiles(db, opts);
333
+ const fileOwners = allFiles.map((file) => ({ file, owners: matchOwners(file, parsed.rules) }));
334
+ const { ownerIndex, ownedCount } = buildOwnerIndex(fileOwners);
213
335
 
214
- // Filter files if --owner specified
215
- let filteredFiles = fileOwners;
216
- if (opts.owner) {
217
- filteredFiles = fileOwners.filter((fo) => fo.owners.includes(opts.owner!));
218
- }
336
+ // Stage 2: apply optional --owner filter
337
+ const filteredFiles = opts.owner
338
+ ? fileOwners.filter((fo) => fo.owners.includes(opts.owner!))
339
+ : fileOwners;
219
340
 
220
- // Get symbols for filtered files
341
+ // Stage 3: load symbols for filtered files
221
342
  const fileSet = new Set(filteredFiles.map((fo) => fo.file));
222
- let symbols = (
223
- db.prepare('SELECT name, kind, file, line FROM nodes').all() as {
224
- name: string;
225
- kind: string;
226
- file: string;
227
- line: number;
228
- }[]
229
- ).filter((n) => fileSet.has(n.file));
230
-
231
- if (opts.noTests) symbols = symbols.filter((s) => !isTestFile(s.file));
232
- if (opts.kind) symbols = symbols.filter((s) => s.kind === opts.kind);
233
-
234
- const symbolsWithOwners = symbols.map((s) => ({
235
- ...s,
236
- owners: matchOwners(s.file, parsed.rules),
237
- }));
238
-
239
- // Boundary analysis — cross-owner call edges
240
- const boundaries: {
241
- from: { name: string; kind: string; file: string; line: number; owners: string[] };
242
- to: { name: string; kind: string; file: string; line: number; owners: string[] };
243
- edgeKind: string;
244
- }[] = [];
245
- if (opts.boundary) {
246
- const edges = db
247
- .prepare(
248
- `SELECT e.id, e.kind AS edgeKind,
249
- s.name AS srcName, s.kind AS srcKind, s.file AS srcFile, s.line AS srcLine,
250
- t.name AS tgtName, t.kind AS tgtKind, t.file AS tgtFile, t.line AS tgtLine
251
- FROM edges e
252
- JOIN nodes s ON e.source_id = s.id
253
- JOIN nodes t ON e.target_id = t.id
254
- WHERE e.kind = 'calls'`,
255
- )
256
- .all() as {
257
- id: number;
258
- edgeKind: string;
259
- srcName: string;
260
- srcKind: string;
261
- srcFile: string;
262
- srcLine: number;
263
- tgtName: string;
264
- tgtKind: string;
265
- tgtFile: string;
266
- tgtLine: number;
267
- }[];
268
-
269
- for (const e of edges) {
270
- if (opts.noTests && (isTestFile(e.srcFile) || isTestFile(e.tgtFile))) continue;
271
- const srcOwners = matchOwners(e.srcFile, parsed.rules);
272
- const tgtOwners = matchOwners(e.tgtFile, parsed.rules);
273
- // Cross-boundary: different owner sets
274
- const srcKey = srcOwners.sort().join(',');
275
- const tgtKey = tgtOwners.sort().join(',');
276
- if (srcKey !== tgtKey) {
277
- boundaries.push({
278
- from: {
279
- name: e.srcName,
280
- kind: e.srcKind,
281
- file: e.srcFile,
282
- line: e.srcLine,
283
- owners: srcOwners,
284
- },
285
- to: {
286
- name: e.tgtName,
287
- kind: e.tgtKind,
288
- file: e.tgtFile,
289
- line: e.tgtLine,
290
- owners: tgtOwners,
291
- },
292
- edgeKind: e.edgeKind,
293
- });
294
- }
295
- }
296
- }
343
+ const symbolsWithOwners = loadSymbolsForFiles(db, fileSet, opts, parsed.rules);
297
344
 
298
- // Summary
299
- const byOwner = [...ownerIndex.entries()]
300
- .map(([owner, files]) => ({ owner, fileCount: files.length }))
301
- .sort((a, b) => b.fileCount - a.fileCount);
345
+ // Stage 4: optional boundary analysis (cross-owner call edges)
346
+ const boundaries = opts.boundary
347
+ ? computeOwnerBoundaries(db, parsed.rules, opts.noTests ?? false)
348
+ : [];
302
349
 
303
350
  return {
304
351
  codeownersFile: parsed.path,
305
352
  files: filteredFiles,
306
353
  symbols: symbolsWithOwners,
307
354
  boundaries,
308
- summary: {
309
- totalFiles: allFiles.length,
310
- ownedFiles: ownedCount,
311
- unownedFiles: allFiles.length - ownedCount,
312
- coveragePercent: allFiles.length > 0 ? Math.round((ownedCount / allFiles.length) * 100) : 0,
313
- ownerCount: ownerIndex.size,
314
- byOwner,
315
- },
355
+ summary: buildOwnersSummary(allFiles.length, ownedCount, ownerIndex),
316
356
  };
317
357
  } finally {
318
358
  db.close();