@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
@@ -235,30 +235,23 @@ interface EvaluateBoundariesOpts {
235
235
  noTests?: boolean;
236
236
  }
237
237
 
238
- export function evaluateBoundaries(
239
- db: BetterSqlite3Database,
240
- boundaryConfig: BoundaryConfig | undefined,
241
- opts: EvaluateBoundariesOpts = {},
242
- ): { violations: BoundaryViolation[]; violationCount: number } {
243
- if (!boundaryConfig) return { violations: [], violationCount: 0 };
244
-
245
- const { valid, errors } = validateBoundaryConfig(boundaryConfig);
246
- if (!valid) {
247
- throw new BoundaryError(`Invalid boundary configuration: ${errors.join('; ')}`);
248
- }
249
-
250
- const modules = resolveModules(boundaryConfig);
251
- if (modules.size === 0) return { violations: [], violationCount: 0 };
252
-
253
- let allRules: BoundaryRule[] = [];
254
- if (boundaryConfig.preset) {
255
- allRules = generatePresetRules(modules, boundaryConfig.preset);
256
- }
238
+ function collectAllRules(
239
+ boundaryConfig: BoundaryConfig,
240
+ modules: Map<string, ResolvedModule>,
241
+ ): BoundaryRule[] {
242
+ const rules: BoundaryRule[] = boundaryConfig.preset
243
+ ? generatePresetRules(modules, boundaryConfig.preset)
244
+ : [];
257
245
  if (boundaryConfig.rules && Array.isArray(boundaryConfig.rules)) {
258
- allRules = allRules.concat(boundaryConfig.rules);
246
+ return rules.concat(boundaryConfig.rules);
259
247
  }
260
- if (allRules.length === 0) return { violations: [], violationCount: 0 };
248
+ return rules;
249
+ }
261
250
 
251
+ function loadImportEdges(
252
+ db: BetterSqlite3Database,
253
+ opts: EvaluateBoundariesOpts,
254
+ ): Array<{ source: string; target: string }> {
262
255
  let edges: Array<{ source: string; target: string }>;
263
256
  try {
264
257
  edges = db
@@ -281,38 +274,63 @@ export function evaluateBoundaries(
281
274
  const scope = new Set(opts.scopeFiles);
282
275
  edges = edges.filter((e) => scope.has(e.source));
283
276
  }
277
+ return edges;
278
+ }
284
279
 
285
- const violations: BoundaryViolation[] = [];
280
+ function ruleViolated(rule: BoundaryRule, toModule: string): boolean {
281
+ if (rule.notTo?.includes(toModule)) return true;
282
+ if (rule.onlyTo && !rule.onlyTo.includes(toModule)) return true;
283
+ return false;
284
+ }
286
285
 
287
- for (const edge of edges) {
288
- const fromModule = classifyFile(edge.source, modules);
289
- const toModule = classifyFile(edge.target, modules);
286
+ function emitEdgeViolations(
287
+ edge: { source: string; target: string },
288
+ fromModule: string,
289
+ toModule: string,
290
+ allRules: BoundaryRule[],
291
+ violations: BoundaryViolation[],
292
+ ): void {
293
+ for (const rule of allRules) {
294
+ if (rule.from !== fromModule) continue;
295
+ if (!ruleViolated(rule, toModule)) continue;
296
+ violations.push({
297
+ rule: 'boundaries',
298
+ name: `${fromModule} -> ${toModule}`,
299
+ file: edge.source,
300
+ targetFile: edge.target,
301
+ message: rule.message || `${fromModule} must not depend on ${toModule}`,
302
+ value: 1,
303
+ threshold: 0,
304
+ });
305
+ }
306
+ }
290
307
 
291
- if (!fromModule || !toModule) continue;
308
+ export function evaluateBoundaries(
309
+ db: BetterSqlite3Database,
310
+ boundaryConfig: BoundaryConfig | undefined,
311
+ opts: EvaluateBoundariesOpts = {},
312
+ ): { violations: BoundaryViolation[]; violationCount: number } {
313
+ if (!boundaryConfig) return { violations: [], violationCount: 0 };
292
314
 
293
- for (const rule of allRules) {
294
- if (rule.from !== fromModule) continue;
315
+ const { valid, errors } = validateBoundaryConfig(boundaryConfig);
316
+ if (!valid) {
317
+ throw new BoundaryError(`Invalid boundary configuration: ${errors.join('; ')}`);
318
+ }
295
319
 
296
- let isViolation = false;
320
+ const modules = resolveModules(boundaryConfig);
321
+ if (modules.size === 0) return { violations: [], violationCount: 0 };
297
322
 
298
- if (rule.notTo?.includes(toModule)) {
299
- isViolation = true;
300
- } else if (rule.onlyTo && !rule.onlyTo.includes(toModule)) {
301
- isViolation = true;
302
- }
323
+ const allRules = collectAllRules(boundaryConfig, modules);
324
+ if (allRules.length === 0) return { violations: [], violationCount: 0 };
303
325
 
304
- if (isViolation) {
305
- violations.push({
306
- rule: 'boundaries',
307
- name: `${fromModule} -> ${toModule}`,
308
- file: edge.source,
309
- targetFile: edge.target,
310
- message: rule.message || `${fromModule} must not depend on ${toModule}`,
311
- value: 1,
312
- threshold: 0,
313
- });
314
- }
315
- }
326
+ const edges = loadImportEdges(db, opts);
327
+ const violations: BoundaryViolation[] = [];
328
+
329
+ for (const edge of edges) {
330
+ const fromModule = classifyFile(edge.source, modules);
331
+ const toModule = classifyFile(edge.target, modules);
332
+ if (!fromModule || !toModule) continue;
333
+ emitEdgeViolations(edge, fromModule, toModule, allRules, violations);
316
334
  }
317
335
 
318
336
  return { violations, violationCount: violations.length };
@@ -18,7 +18,13 @@ import {
18
18
  } from '../db/index.js';
19
19
  import { debug, info } from '../infrastructure/logger.js';
20
20
  import { paginateResult } from '../shared/paginate.js';
21
- import type { BetterSqlite3Database, Definition, NodeRow, TreeSitterNode } from '../types.js';
21
+ import type {
22
+ BetterSqlite3Database,
23
+ CfgRulesConfig,
24
+ Definition,
25
+ NodeRow,
26
+ TreeSitterNode,
27
+ } from '../types.js';
22
28
  import { findNodes } from './shared/find-nodes.js';
23
29
 
24
30
  export { _makeCfgRules as makeCfgRules, CFG_RULES };
@@ -122,9 +128,8 @@ async function initCfgParsers(
122
128
  let getParserFn: unknown = null;
123
129
 
124
130
  if (needsFallback) {
125
- const { createParsers } = await import('../domain/parser.js');
126
- parsers = await createParsers();
127
131
  const mod = await import('../domain/parser.js');
132
+ parsers = await mod.createParsers();
128
133
  getParserFn = mod.getParser;
129
134
  }
130
135
 
@@ -187,7 +192,7 @@ interface VisitorCfgResult {
187
192
 
188
193
  function buildVisitorCfgMap(
189
194
  tree: { rootNode: TreeSitterNode } | null | undefined,
190
- cfgRules: unknown,
195
+ cfgRules: CfgRulesConfig,
191
196
  symbols: FileSymbols,
192
197
  langId: string,
193
198
  ): Map<number, VisitorCfgResult[]> | null {
@@ -203,9 +208,8 @@ function buildVisitorCfgMap(
203
208
  if (!needsVisitor) return null;
204
209
 
205
210
  const visitor = createCfgVisitor(cfgRules);
206
- const typedRules = cfgRules as { functionNodes: string[] };
207
211
  const walkerOpts = {
208
- functionNodeTypes: new Set(typedRules.functionNodes),
212
+ functionNodeTypes: new Set(cfgRules.functionNodes),
209
213
  nestingNodeTypes: new Set<string>(),
210
214
  getFunctionName: (node: TreeSitterNode) => {
211
215
  const nameNode = node.childForFieldName?.('name');
@@ -365,79 +369,91 @@ function persistVisitorFileCfg(
365
369
  return count;
366
370
  }
367
371
 
368
- export async function buildCFGData(
372
+ /**
373
+ * Build a single native bulk-insert entry for one definition.
374
+ * Returns null when the def has no CFG blocks or no associated node row.
375
+ */
376
+ function buildNativeCfgEntry(
369
377
  db: BetterSqlite3Database,
370
- fileSymbols: Map<string, FileSymbols>,
371
- rootDir: string,
372
- engineOpts?: {
373
- nativeDb?: { bulkInsertCfg?(entries: Array<Record<string, unknown>>): number };
374
- suspendJsDb?: () => void;
375
- resumeJsDb?: () => void;
376
- },
377
- ): Promise<void> {
378
- // Fast path: when all function/method defs already have native CFG data,
379
- // skip WASM parser init, tree parsing, and JS visitor entirely just persist.
380
- const allNative = allCfgNative(fileSymbols);
378
+ def: Definition,
379
+ relPath: string,
380
+ ): Record<string, unknown> | null {
381
+ if (def.kind !== 'function' && def.kind !== 'method') return null;
382
+ if (!def.line) return null;
383
+
384
+ const nodeId = getFunctionNodeId(db, def.name, relPath, def.line);
385
+ if (!nodeId) return null;
386
+
387
+ const cfg = def.cfg as { blocks?: CfgBuildBlock[]; edges?: CfgBuildEdge[] } | undefined;
388
+ if (!cfg?.blocks?.length) return null;
389
+
390
+ return {
391
+ nodeId,
392
+ blocks: cfg.blocks.map((b) => ({
393
+ index: b.index,
394
+ blockType: b.type,
395
+ startLine: b.startLine ?? undefined,
396
+ endLine: b.endLine ?? undefined,
397
+ label: b.label ?? undefined,
398
+ })),
399
+ edges: (cfg.edges || []).map((e) => ({
400
+ sourceIndex: e.sourceIndex,
401
+ targetIndex: e.targetIndex,
402
+ kind: e.kind,
403
+ })),
404
+ };
405
+ }
381
406
 
382
- // ── Native bulk-insert fast path ──────────────────────────────────────
383
- // The Rust bulkInsertCfg handles delete-before-insert atomically on a
384
- // single rusqlite connection, so there is no dual-connection WAL conflict.
407
+ /**
408
+ * Native bulk-insert fast path. The Rust bulkInsertCfg handles
409
+ * delete-before-insert atomically on a single rusqlite connection, so there
410
+ * is no dual-connection WAL conflict. Returns true if this path handled the
411
+ * request (caller should return early); false to fall through to WASM/JS.
412
+ */
413
+ function tryNativeBulkInsertCfg(
414
+ db: BetterSqlite3Database,
415
+ fileSymbols: Map<string, FileSymbols>,
416
+ engineOpts:
417
+ | {
418
+ nativeDb?: { bulkInsertCfg?(entries: Array<Record<string, unknown>>): number };
419
+ suspendJsDb?: () => void;
420
+ resumeJsDb?: () => void;
421
+ }
422
+ | undefined,
423
+ ): boolean {
385
424
  const nativeDb = engineOpts?.nativeDb;
386
- if (allNative && nativeDb?.bulkInsertCfg) {
387
- const entries: Array<Record<string, unknown>> = [];
388
- for (const [relPath, symbols] of fileSymbols) {
389
- const ext = path.extname(relPath).toLowerCase();
390
- if (!CFG_EXTENSIONS.has(ext)) continue;
425
+ if (!nativeDb?.bulkInsertCfg) return false;
391
426
 
392
- for (const def of symbols.definitions) {
393
- if (def.kind !== 'function' && def.kind !== 'method') continue;
394
- if (!def.line) continue;
395
-
396
- const nodeId = getFunctionNodeId(db, def.name, relPath, def.line);
397
- if (!nodeId) continue;
398
-
399
- const cfg = def.cfg as { blocks?: CfgBuildBlock[]; edges?: CfgBuildEdge[] } | undefined;
400
- if (!cfg?.blocks?.length) continue;
401
-
402
- entries.push({
403
- nodeId,
404
- blocks: cfg.blocks.map((b) => ({
405
- index: b.index,
406
- blockType: b.type,
407
- startLine: b.startLine ?? undefined,
408
- endLine: b.endLine ?? undefined,
409
- label: b.label ?? undefined,
410
- })),
411
- edges: (cfg.edges || []).map((e) => ({
412
- sourceIndex: e.sourceIndex,
413
- targetIndex: e.targetIndex,
414
- kind: e.kind,
415
- })),
416
- });
417
- }
418
- }
427
+ const entries: Array<Record<string, unknown>> = [];
428
+ for (const [relPath, symbols] of fileSymbols) {
429
+ const ext = path.extname(relPath).toLowerCase();
430
+ if (!CFG_EXTENSIONS.has(ext)) continue;
419
431
 
420
- if (entries.length > 0) {
421
- let inserted = 0;
422
- try {
423
- engineOpts?.suspendJsDb?.();
424
- inserted = nativeDb.bulkInsertCfg(entries);
425
- } finally {
426
- engineOpts?.resumeJsDb?.();
427
- }
428
- info(`CFG (native bulk): ${inserted} functions analyzed`);
432
+ for (const def of symbols.definitions) {
433
+ const entry = buildNativeCfgEntry(db, def, relPath);
434
+ if (entry) entries.push(entry);
429
435
  }
430
- return;
431
436
  }
432
437
 
433
- const extToLang = buildExtToLangMap();
434
- let parsers: unknown = null;
435
- let getParserFn: unknown = null;
436
-
437
- if (!allNative) {
438
- ({ parsers, getParserFn } = await initCfgParsers(fileSymbols));
438
+ if (entries.length > 0) {
439
+ let inserted = 0;
440
+ try {
441
+ engineOpts?.suspendJsDb?.();
442
+ inserted = nativeDb.bulkInsertCfg(entries);
443
+ } finally {
444
+ engineOpts?.resumeJsDb?.();
445
+ }
446
+ info(`CFG (native bulk): ${inserted} functions analyzed`);
439
447
  }
448
+ return true;
449
+ }
440
450
 
451
+ interface CfgInsertStatements {
452
+ insertBlock: ReturnType<BetterSqlite3Database['prepare']>;
453
+ insertEdge: ReturnType<BetterSqlite3Database['prepare']>;
454
+ }
455
+
456
+ function prepareCfgInsertStatements(db: BetterSqlite3Database): CfgInsertStatements {
441
457
  const insertBlock = db.prepare(
442
458
  `INSERT INTO cfg_blocks (function_node_id, block_index, block_type, start_line, end_line, label)
443
459
  VALUES (?, ?, ?, ?, ?, ?)`,
@@ -446,15 +462,31 @@ export async function buildCFGData(
446
462
  `INSERT INTO cfg_edges (function_node_id, source_block_id, target_block_id, kind)
447
463
  VALUES (?, ?, ?, ?)`,
448
464
  );
449
- let analyzed = 0;
465
+ return { insertBlock, insertEdge };
466
+ }
450
467
 
468
+ /**
469
+ * Persist CFG for every CFG-eligible file inside a single transaction.
470
+ * Dispatches to native fast path or visitor path per file.
471
+ */
472
+ function persistAllFileCfgs(
473
+ db: BetterSqlite3Database,
474
+ fileSymbols: Map<string, FileSymbols>,
475
+ rootDir: string,
476
+ allNative: boolean,
477
+ extToLang: Map<string, string>,
478
+ parsers: unknown,
479
+ getParserFn: unknown,
480
+ stmts: CfgInsertStatements,
481
+ ): number {
482
+ let analyzed = 0;
451
483
  const tx = db.transaction(() => {
452
484
  for (const [relPath, symbols] of fileSymbols) {
453
485
  const ext = path.extname(relPath).toLowerCase();
454
486
  if (!CFG_EXTENSIONS.has(ext)) continue;
455
487
 
456
488
  if (allNative && !symbols._tree) {
457
- analyzed += persistNativeFileCfg(db, symbols, relPath, insertBlock, insertEdge);
489
+ analyzed += persistNativeFileCfg(db, symbols, relPath, stmts.insertBlock, stmts.insertEdge);
458
490
  continue;
459
491
  }
460
492
 
@@ -466,13 +498,52 @@ export async function buildCFGData(
466
498
  extToLang,
467
499
  parsers,
468
500
  getParserFn,
469
- insertBlock,
470
- insertEdge,
501
+ stmts.insertBlock,
502
+ stmts.insertEdge,
471
503
  );
472
504
  }
473
505
  });
474
-
475
506
  tx();
507
+ return analyzed;
508
+ }
509
+
510
+ export async function buildCFGData(
511
+ db: BetterSqlite3Database,
512
+ fileSymbols: Map<string, FileSymbols>,
513
+ rootDir: string,
514
+ engineOpts?: {
515
+ nativeDb?: { bulkInsertCfg?(entries: Array<Record<string, unknown>>): number };
516
+ suspendJsDb?: () => void;
517
+ resumeJsDb?: () => void;
518
+ },
519
+ ): Promise<void> {
520
+ // Fast path: when all function/method defs already have native CFG data,
521
+ // skip WASM parser init, tree parsing, and JS visitor entirely — just persist.
522
+ const allNative = allCfgNative(fileSymbols);
523
+
524
+ if (allNative && tryNativeBulkInsertCfg(db, fileSymbols, engineOpts)) {
525
+ return;
526
+ }
527
+
528
+ const extToLang = buildExtToLangMap();
529
+ let parsers: unknown = null;
530
+ let getParserFn: unknown = null;
531
+
532
+ if (!allNative) {
533
+ ({ parsers, getParserFn } = await initCfgParsers(fileSymbols));
534
+ }
535
+
536
+ const stmts = prepareCfgInsertStatements(db);
537
+ const analyzed = persistAllFileCfgs(
538
+ db,
539
+ fileSymbols,
540
+ rootDir,
541
+ allNative,
542
+ extToLang,
543
+ parsers,
544
+ getParserFn,
545
+ stmts,
546
+ );
476
547
 
477
548
  if (analyzed > 0) {
478
549
  info(`CFG: ${analyzed} functions analyzed`);
@@ -22,6 +22,29 @@ interface ParsedDiff {
22
22
  newFiles: Set<string>;
23
23
  }
24
24
 
25
+ const HUNK_RE = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/;
26
+ const NEW_FILE_RE = /^\+\+\+ b\/(.+)/;
27
+
28
+ function pushHunkRanges(
29
+ line: string,
30
+ currentFile: string,
31
+ changedRanges: Map<string, DiffRange[]>,
32
+ oldRanges: Map<string, DiffRange[]>,
33
+ ): void {
34
+ const hunkMatch = line.match(HUNK_RE);
35
+ if (!hunkMatch) return;
36
+ const oldStart = parseInt(hunkMatch[1]!, 10);
37
+ const oldCount = parseInt(hunkMatch[2] || '1', 10);
38
+ if (oldCount > 0) {
39
+ oldRanges.get(currentFile)!.push({ start: oldStart, end: oldStart + oldCount - 1 });
40
+ }
41
+ const newStart = parseInt(hunkMatch[3]!, 10);
42
+ const newCount = parseInt(hunkMatch[4] || '1', 10);
43
+ if (newCount > 0) {
44
+ changedRanges.get(currentFile)!.push({ start: newStart, end: newStart + newCount - 1 });
45
+ }
46
+ }
47
+
25
48
  export function parseDiffOutput(diffOutput: string): ParsedDiff {
26
49
  const changedRanges = new Map<string, DiffRange[]>();
27
50
  const oldRanges = new Map<string, DiffRange[]>();
@@ -38,7 +61,7 @@ export function parseDiffOutput(diffOutput: string): ParsedDiff {
38
61
  prevIsDevNull = false;
39
62
  continue;
40
63
  }
41
- const fileMatch = line.match(/^\+\+\+ b\/(.+)/);
64
+ const fileMatch = line.match(NEW_FILE_RE);
42
65
  if (fileMatch) {
43
66
  currentFile = fileMatch[1]!;
44
67
  if (!changedRanges.has(currentFile)) changedRanges.set(currentFile, []);
@@ -47,19 +70,7 @@ export function parseDiffOutput(diffOutput: string): ParsedDiff {
47
70
  prevIsDevNull = false;
48
71
  continue;
49
72
  }
50
- const hunkMatch = line.match(/^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/);
51
- if (hunkMatch && currentFile) {
52
- const oldStart = parseInt(hunkMatch[1]!, 10);
53
- const oldCount = parseInt(hunkMatch[2] || '1', 10);
54
- if (oldCount > 0) {
55
- oldRanges.get(currentFile)!.push({ start: oldStart, end: oldStart + oldCount - 1 });
56
- }
57
- const newStart = parseInt(hunkMatch[3]!, 10);
58
- const newCount = parseInt(hunkMatch[4] || '1', 10);
59
- if (newCount > 0) {
60
- changedRanges.get(currentFile)!.push({ start: newStart, end: newStart + newCount - 1 });
61
- }
62
- }
73
+ if (currentFile) pushHunkRanges(line, currentFile, changedRanges, oldRanges);
63
74
  }
64
75
  return { changedRanges, oldRanges, newFiles };
65
76
  }
@@ -96,6 +107,26 @@ interface BlastRadiusResult {
96
107
  violations: BlastRadiusViolation[];
97
108
  }
98
109
 
110
+ type DefRow = {
111
+ id: number;
112
+ name: string;
113
+ kind: string;
114
+ file: string;
115
+ line: number;
116
+ end_line: number | null;
117
+ };
118
+
119
+ function rangesOverlap(defLine: number, endLine: number, ranges: DiffRange[]): boolean {
120
+ for (const range of ranges) {
121
+ if (range.start <= endLine && range.end >= defLine) return true;
122
+ }
123
+ return false;
124
+ }
125
+
126
+ function defEndLine(def: DefRow, nextDef: DefRow | undefined): number {
127
+ return def.end_line || (nextDef ? nextDef.line - 1 : 999999);
128
+ }
129
+
99
130
  export function checkMaxBlastRadius(
100
131
  db: BetterSqlite3Database,
101
132
  changedRanges: Map<string, DiffRange[]>,
@@ -105,34 +136,18 @@ export function checkMaxBlastRadius(
105
136
  ): BlastRadiusResult {
106
137
  const violations: BlastRadiusViolation[] = [];
107
138
  let maxFound = 0;
139
+ const defsStmt = db.prepare(
140
+ `SELECT * FROM nodes WHERE file = ? AND kind IN ('function', 'method', 'class') ORDER BY line`,
141
+ );
108
142
 
109
143
  for (const [file, ranges] of changedRanges) {
110
144
  if (noTests && isTestFile(file)) continue;
111
- const defs = db
112
- .prepare(
113
- `SELECT * FROM nodes WHERE file = ? AND kind IN ('function', 'method', 'class') ORDER BY line`,
114
- )
115
- .all(file) as Array<{
116
- id: number;
117
- name: string;
118
- kind: string;
119
- file: string;
120
- line: number;
121
- end_line: number | null;
122
- }>;
145
+ const defs = defsStmt.all(file) as DefRow[];
123
146
 
124
147
  for (let i = 0; i < defs.length; i++) {
125
148
  const def = defs[i]!;
126
- const nextDef = defs[i + 1];
127
- const endLine = def.end_line || (nextDef ? nextDef.line - 1 : 999999);
128
- let overlaps = false;
129
- for (const range of ranges) {
130
- if (range.start <= endLine && range.end >= def.line) {
131
- overlaps = true;
132
- break;
133
- }
134
- }
135
- if (!overlaps) continue;
149
+ const endLine = defEndLine(def, defs[i + 1]);
150
+ if (!rangesOverlap(def.line, endLine, ranges)) continue;
136
151
 
137
152
  const { totalDependents: totalCallers } = bfsTransitiveCallers(db, def.id, {
138
153
  noTests,
@@ -364,11 +379,13 @@ function runPredicates(
364
379
  return predicates;
365
380
  }
366
381
 
367
- const EMPTY_CHECK: CheckResult = {
368
- predicates: [],
369
- summary: { total: 0, passed: 0, failed: 0, changedFiles: 0, newFiles: 0 },
370
- passed: true,
371
- };
382
+ function makeEmptyCheck(): CheckResult {
383
+ return {
384
+ predicates: [],
385
+ summary: { total: 0, passed: 0, failed: 0, changedFiles: 0, newFiles: 0 },
386
+ passed: true,
387
+ };
388
+ }
372
389
 
373
390
  export function checkData(customDbPath: string | undefined, opts: CheckOpts = {}): CheckResult {
374
391
  const db = openReadonlyOrFail(customDbPath);
@@ -394,10 +411,10 @@ export function checkData(customDbPath: string | undefined, opts: CheckOpts = {}
394
411
  return { error: `Failed to run git diff: ${(e as Error).message}` };
395
412
  }
396
413
 
397
- if (!diffOutput.trim()) return EMPTY_CHECK;
414
+ if (!diffOutput.trim()) return makeEmptyCheck();
398
415
 
399
416
  const diff = parseDiffOutput(diffOutput);
400
- if (diff.changedRanges.size === 0) return EMPTY_CHECK;
417
+ if (diff.changedRanges.size === 0) return makeEmptyCheck();
401
418
 
402
419
  const predicates = runPredicates(db, diff, flags, repoRoot, noTests, maxDepth);
403
420