@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
@@ -137,77 +137,50 @@ export function computeCoChanges(
137
137
  return { pairs: results, fileCommitCounts };
138
138
  }
139
139
 
140
- export function analyzeCoChanges(
141
- customDbPath?: string,
142
- opts: {
143
- since?: string;
144
- minSupport?: number;
145
- maxFilesPerCommit?: number;
146
- full?: boolean;
147
- } = {},
148
- ):
149
- | { pairsFound: number; commitsScanned: number; since: string; minSupport: number }
150
- | { error: string } {
151
- const dbPath = findDbPath(customDbPath);
152
- const db = openDb(dbPath);
153
- initSchema(db);
154
-
155
- const repoRoot = path.resolve(path.dirname(dbPath), '..');
156
-
157
- if (!fs.existsSync(path.join(repoRoot, '.git'))) {
158
- closeDb(db);
159
- return { error: `Not a git repository: ${repoRoot}` };
160
- }
161
-
162
- const since = opts.since || '1 year ago';
163
- const minSupport = opts.minSupport ?? 3;
164
- const maxFilesPerCommit = opts.maxFilesPerCommit ?? 50;
165
-
166
- // Check for incremental state
167
- let afterSha: string | null = null;
168
- if (!opts.full) {
169
- try {
170
- const row = db
171
- .prepare<{ value: string }>(
172
- "SELECT value FROM co_change_meta WHERE key = 'last_analyzed_commit'",
173
- )
174
- .get();
175
- if (row) afterSha = row.value;
176
- } catch {
177
- /* table may not exist yet */
178
- }
140
+ /** Read the SHA of the most recently analyzed commit (incremental state). */
141
+ function loadLastAnalyzedSha(db: BetterSqlite3Database): string | null {
142
+ try {
143
+ const row = db
144
+ .prepare<{ value: string }>(
145
+ "SELECT value FROM co_change_meta WHERE key = 'last_analyzed_commit'",
146
+ )
147
+ .get();
148
+ return row ? row.value : null;
149
+ } catch {
150
+ /* table may not exist yet */
151
+ return null;
179
152
  }
153
+ }
180
154
 
181
- // If full re-scan, clear existing data
182
- if (opts.full) {
183
- db.exec('DELETE FROM co_changes');
184
- db.exec('DELETE FROM co_change_meta');
185
- db.exec('DELETE FROM file_commit_counts');
186
- }
155
+ /** Wipe all co-change tables for a full re-scan. */
156
+ function clearCoChangeTables(db: BetterSqlite3Database): void {
157
+ db.exec('DELETE FROM co_changes');
158
+ db.exec('DELETE FROM co_change_meta');
159
+ db.exec('DELETE FROM file_commit_counts');
160
+ }
187
161
 
188
- // Collect known files from the graph for filtering
189
- let knownFiles: Set<string> | null = null;
162
+ /** Collect the set of files currently tracked by the graph for filtering. */
163
+ function loadKnownFiles(db: BetterSqlite3Database): Set<string> | null {
190
164
  try {
191
165
  const rows = db.prepare<{ file: string }>('SELECT DISTINCT file FROM nodes').all();
192
- knownFiles = new Set(rows.map((r) => r.file));
166
+ return new Set(rows.map((r) => r.file));
193
167
  } catch {
194
168
  /* nodes table may not exist */
169
+ return null;
195
170
  }
171
+ }
196
172
 
197
- const { commits } = scanGitHistory(repoRoot, { since, afterSha });
198
- const { pairs: coChanges, fileCommitCounts } = computeCoChanges(commits, {
199
- minSupport,
200
- maxFilesPerCommit,
201
- knownFiles,
202
- });
203
-
204
- // Upsert per-file commit counts so Jaccard can be recomputed from totals
173
+ /** Upsert per-file commit counts and pair counts (Jaccard recomputed later). */
174
+ function persistCoChangeResults(
175
+ db: BetterSqlite3Database,
176
+ fileCommitCounts: Map<string, number>,
177
+ coChanges: Map<string, CoChangePair>,
178
+ ): void {
205
179
  const fileCountUpsert = db.prepare(`
206
180
  INSERT INTO file_commit_counts (file, commit_count) VALUES (?, ?)
207
181
  ON CONFLICT(file) DO UPDATE SET commit_count = commit_count + excluded.commit_count
208
182
  `);
209
183
 
210
- // Upsert pair counts (accumulate commit_count, jaccard placeholder — recomputed below)
211
184
  const pairUpsert = db.prepare(`
212
185
  INSERT INTO co_changes (file_a, file_b, commit_count, jaccard, last_commit_epoch)
213
186
  VALUES (?, ?, ?, 0, ?)
@@ -226,24 +199,31 @@ export function analyzeCoChanges(
226
199
  }
227
200
  });
228
201
  insertMany();
202
+ }
229
203
 
230
- // Recompute Jaccard for all affected pairs from total file commit counts
231
- const affectedFiles = [...fileCommitCounts.keys()];
232
- if (affectedFiles.length > 0) {
233
- const ph = affectedFiles.map(() => '?').join(',');
234
- db.prepare(`
235
- UPDATE co_changes SET jaccard = (
236
- SELECT CAST(co_changes.commit_count AS REAL) / (
237
- COALESCE(fa.commit_count, 0) + COALESCE(fb.commit_count, 0) - co_changes.commit_count
238
- )
239
- FROM file_commit_counts fa, file_commit_counts fb
240
- WHERE fa.file = co_changes.file_a AND fb.file = co_changes.file_b
204
+ /** Recompute Jaccard for every pair touching any file in `affectedFiles`. */
205
+ function recomputeJaccardForAffected(db: BetterSqlite3Database, affectedFiles: string[]): void {
206
+ if (affectedFiles.length === 0) return;
207
+ const ph = affectedFiles.map(() => '?').join(',');
208
+ db.prepare(`
209
+ UPDATE co_changes SET jaccard = (
210
+ SELECT CAST(co_changes.commit_count AS REAL) / (
211
+ COALESCE(fa.commit_count, 0) + COALESCE(fb.commit_count, 0) - co_changes.commit_count
241
212
  )
242
- WHERE file_a IN (${ph}) OR file_b IN (${ph})
243
- `).run(...affectedFiles, ...affectedFiles);
244
- }
213
+ FROM file_commit_counts fa, file_commit_counts fb
214
+ WHERE fa.file = co_changes.file_a AND fb.file = co_changes.file_b
215
+ )
216
+ WHERE file_a IN (${ph}) OR file_b IN (${ph})
217
+ `).run(...affectedFiles, ...affectedFiles);
218
+ }
245
219
 
246
- // Update metadata
220
+ /** Update co_change_meta with the latest analyzer run parameters. */
221
+ function updateCoChangeMeta(
222
+ db: BetterSqlite3Database,
223
+ commits: CommitEntry[],
224
+ since: string,
225
+ minSupport: number,
226
+ ): void {
247
227
  const metaUpsert = db.prepare(`
248
228
  INSERT INTO co_change_meta (key, value) VALUES (?, ?)
249
229
  ON CONFLICT(key) DO UPDATE SET value = excluded.value
@@ -254,6 +234,49 @@ export function analyzeCoChanges(
254
234
  metaUpsert.run('analyzed_at', new Date().toISOString());
255
235
  metaUpsert.run('since', since);
256
236
  metaUpsert.run('min_support', String(minSupport));
237
+ }
238
+
239
+ export function analyzeCoChanges(
240
+ customDbPath?: string,
241
+ opts: {
242
+ since?: string;
243
+ minSupport?: number;
244
+ maxFilesPerCommit?: number;
245
+ full?: boolean;
246
+ } = {},
247
+ ):
248
+ | { pairsFound: number; commitsScanned: number; since: string; minSupport: number }
249
+ | { error: string } {
250
+ const dbPath = findDbPath(customDbPath);
251
+ const db = openDb(dbPath);
252
+ initSchema(db);
253
+
254
+ const repoRoot = path.resolve(path.dirname(dbPath), '..');
255
+
256
+ if (!fs.existsSync(path.join(repoRoot, '.git'))) {
257
+ closeDb(db);
258
+ return { error: `Not a git repository: ${repoRoot}` };
259
+ }
260
+
261
+ const since = opts.since || '1 year ago';
262
+ const minSupport = opts.minSupport ?? 3;
263
+ const maxFilesPerCommit = opts.maxFilesPerCommit ?? 50;
264
+
265
+ const afterSha = opts.full ? null : loadLastAnalyzedSha(db);
266
+ if (opts.full) clearCoChangeTables(db);
267
+
268
+ const knownFiles = loadKnownFiles(db);
269
+
270
+ const { commits } = scanGitHistory(repoRoot, { since, afterSha });
271
+ const { pairs: coChanges, fileCommitCounts } = computeCoChanges(commits, {
272
+ minSupport,
273
+ maxFilesPerCommit,
274
+ knownFiles,
275
+ });
276
+
277
+ persistCoChangeResults(db, fileCommitCounts, coChanges);
278
+ recomputeJaccardForAffected(db, [...fileCommitCounts.keys()]);
279
+ updateCoChangeMeta(db, commits, since, minSupport);
257
280
 
258
281
  const totalPairs = db
259
282
  .prepare<{ cnt: number }>('SELECT COUNT(*) as cnt FROM co_changes')
@@ -31,44 +31,36 @@ const COMPLEXITY_EXTENSIONS = buildExtensionSet(COMPLEXITY_RULES);
31
31
 
32
32
  // ─── Halstead Metrics Computation ─────────────────────────────────────────
33
33
 
34
- export function computeHalsteadMetrics(
35
- functionNode: TreeSitterNode,
36
- language: string,
37
- ): HalsteadDerivedMetrics | null {
38
- const rules = HALSTEAD_RULES.get(language) as HalsteadRules | undefined;
39
- if (!rules) return null;
40
-
41
- const operators = new Map<string, number>(); // type -> count
42
- const operands = new Map<string, number>(); // text -> count
43
-
44
- function walk(node: TreeSitterNode | null): void {
45
- if (!node) return;
46
-
47
- // Skip type annotation subtrees
48
- if (rules?.skipTypes.has(node.type)) return;
34
+ /** Classify a tree-sitter node as a Halstead operator or operand,
35
+ * updating the running counts. Pure helper extracted from computeHalsteadMetrics
36
+ * to keep the dispatcher thin. */
37
+ function classifyHalsteadToken(
38
+ node: TreeSitterNode,
39
+ rules: HalsteadRules,
40
+ operators: Map<string, number>,
41
+ operands: Map<string, number>,
42
+ ): void {
43
+ // Compound operators (non-leaf): count the node type as an operator
44
+ if (rules.compoundOperators.has(node.type)) {
45
+ operators.set(node.type, (operators.get(node.type) || 0) + 1);
46
+ }
49
47
 
50
- // Compound operators (non-leaf): count the node type as an operator
51
- if (rules?.compoundOperators.has(node.type)) {
48
+ // Leaf nodes: classify as operator or operand
49
+ if (node.childCount === 0) {
50
+ if (rules.operatorLeafTypes.has(node.type)) {
52
51
  operators.set(node.type, (operators.get(node.type) || 0) + 1);
53
- }
54
-
55
- // Leaf nodes: classify as operator or operand
56
- if (node.childCount === 0) {
57
- if (rules?.operatorLeafTypes.has(node.type)) {
58
- operators.set(node.type, (operators.get(node.type) || 0) + 1);
59
- } else if (rules?.operandLeafTypes.has(node.type)) {
60
- const text = node.text;
61
- operands.set(text, (operands.get(text) || 0) + 1);
62
- }
63
- }
64
-
65
- for (let i = 0; i < node.childCount; i++) {
66
- walk(node.child(i));
52
+ } else if (rules.operandLeafTypes.has(node.type)) {
53
+ const text = node.text;
54
+ operands.set(text, (operands.get(text) || 0) + 1);
67
55
  }
68
56
  }
57
+ }
69
58
 
70
- walk(functionNode);
71
-
59
+ /** Build a HalsteadDerivedMetrics summary from the raw operator/operand counts. */
60
+ function summarizeHalsteadCounts(
61
+ operators: Map<string, number>,
62
+ operands: Map<string, number>,
63
+ ): HalsteadDerivedMetrics {
72
64
  const n1 = operators.size; // distinct operators
73
65
  const n2 = operands.size; // distinct operands
74
66
  let bigN1 = 0; // total operators
@@ -79,7 +71,6 @@ export function computeHalsteadMetrics(
79
71
  const vocabulary = n1 + n2;
80
72
  const length = bigN1 + bigN2;
81
73
 
82
- // Guard against zero
83
74
  const volume = vocabulary > 0 ? length * Math.log2(vocabulary) : 0;
84
75
  const difficulty = n2 > 0 ? (n1 / 2) * (bigN2 / n2) : 0;
85
76
  const effort = difficulty * volume;
@@ -99,6 +90,31 @@ export function computeHalsteadMetrics(
99
90
  };
100
91
  }
101
92
 
93
+ export function computeHalsteadMetrics(
94
+ functionNode: TreeSitterNode,
95
+ language: string,
96
+ ): HalsteadDerivedMetrics | null {
97
+ const rules = HALSTEAD_RULES.get(language) as HalsteadRules | undefined;
98
+ if (!rules) return null;
99
+
100
+ const operators = new Map<string, number>(); // type -> count
101
+ const operands = new Map<string, number>(); // text -> count
102
+
103
+ function walk(node: TreeSitterNode | null): void {
104
+ if (!node) return;
105
+ // Skip type annotation subtrees
106
+ if (rules?.skipTypes.has(node.type)) return;
107
+ classifyHalsteadToken(node, rules as HalsteadRules, operators, operands);
108
+ for (let i = 0; i < node.childCount; i++) {
109
+ walk(node.child(i));
110
+ }
111
+ }
112
+
113
+ walk(functionNode);
114
+
115
+ return summarizeHalsteadCounts(operators, operands);
116
+ }
117
+
102
118
  // ─── LOC Metrics Computation ──────────────────────────────────────────────
103
119
  // Delegated to ast-analysis/metrics.js; re-exported for backward compatibility.
104
120
  export const computeLOCMetrics = _computeLOCMetrics;
@@ -535,6 +551,89 @@ function upsertAstComplexity(
535
551
  return 1;
536
552
  }
537
553
 
554
+ /** Decision outcome for a single definition during native bulk-row collection.
555
+ * - 'skip': the definition is legitimately ignorable (non-function, missing line,
556
+ * interface stub, unsupported language).
557
+ * - 'fallback': a genuine function body is missing precomputed complexity —
558
+ * the whole native fast path must abort to JS.
559
+ * - 'emit': the definition has complexity data and a row was (or will be) appended. */
560
+ type NativeRowDecision = 'skip' | 'fallback' | 'emit';
561
+
562
+ /** Classify a definition relative to the native bulk path. Returns
563
+ * 'skip' to ignore it, 'fallback' to bail out, or 'emit' if the row should be added. */
564
+ function classifyDefinitionForNativeBulk(
565
+ def: FileSymbols['definitions'][0],
566
+ langSupported: boolean,
567
+ ): NativeRowDecision {
568
+ if (def.kind !== 'function' && def.kind !== 'method') return 'skip';
569
+ if (!def.line) return 'skip';
570
+ if (!def.complexity) {
571
+ // Interface/type property signatures and single-line stubs are extracted
572
+ // as methods but the native engine correctly never assigns complexity.
573
+ // Mirror the leniency in initWasmParsersIfNeeded to avoid bailing out
574
+ // of the native bulk-insert path for every TypeScript codebase (#846).
575
+ if (def.name.includes('.') || !def.endLine || def.endLine <= def.line) return 'skip';
576
+ // Languages without complexity rules will never have data — skip them
577
+ // rather than bailing out of the entire native bulk path.
578
+ if (!langSupported) return 'skip';
579
+ return 'fallback'; // genuine function body missing complexity — needs JS fallback
580
+ }
581
+ return 'emit';
582
+ }
583
+
584
+ /** Build a single native-bulk row from a definition with complexity data. */
585
+ function buildNativeBulkRow(
586
+ nodeId: number,
587
+ def: FileSymbols['definitions'][0],
588
+ ): Record<string, unknown> {
589
+ const ch = def.complexity?.halstead;
590
+ const cl = def.complexity?.loc;
591
+ return {
592
+ nodeId,
593
+ cognitive: def.complexity?.cognitive ?? 0,
594
+ cyclomatic: def.complexity?.cyclomatic ?? 0,
595
+ maxNesting: def.complexity?.maxNesting ?? 0,
596
+ loc: cl ? cl.loc : 0,
597
+ sloc: cl ? cl.sloc : 0,
598
+ commentLines: cl ? cl.commentLines : 0,
599
+ halsteadN1: ch ? ch.n1 : 0,
600
+ halsteadN2: ch ? ch.n2 : 0,
601
+ halsteadBigN1: ch ? ch.bigN1 : 0,
602
+ halsteadBigN2: ch ? ch.bigN2 : 0,
603
+ halsteadVocabulary: ch ? ch.vocabulary : 0,
604
+ halsteadLength: ch ? ch.length : 0,
605
+ halsteadVolume: ch ? ch.volume : 0,
606
+ halsteadDifficulty: ch ? ch.difficulty : 0,
607
+ halsteadEffort: ch ? ch.effort : 0,
608
+ halsteadBugs: ch ? ch.bugs : 0,
609
+ maintainabilityIndex: def.complexity?.maintainabilityIndex ?? 0,
610
+ };
611
+ }
612
+
613
+ /** Try to collect a single file's definitions into native-bulk rows.
614
+ * Returns 'fallback' if any definition forces a JS fallback. */
615
+ function collectFileBulkRows(
616
+ db: BetterSqlite3Database,
617
+ relPath: string,
618
+ symbols: FileSymbols,
619
+ rows: Array<Record<string, unknown>>,
620
+ ): NativeRowDecision {
621
+ const ext = path.extname(relPath).toLowerCase();
622
+ const langId = symbols._langId || '';
623
+ const langSupported = COMPLEXITY_EXTENSIONS.has(ext) || COMPLEXITY_RULES.has(langId);
624
+
625
+ for (const def of symbols.definitions) {
626
+ const decision = classifyDefinitionForNativeBulk(def, langSupported);
627
+ if (decision === 'skip') continue;
628
+ if (decision === 'fallback') return 'fallback';
629
+
630
+ const nodeId = getFunctionNodeId(db, def.name, relPath, def.line);
631
+ if (!nodeId) continue;
632
+ rows.push(buildNativeBulkRow(nodeId, def));
633
+ }
634
+ return 'emit';
635
+ }
636
+
538
637
  /** Collect native bulk-insert rows from precomputed complexity data.
539
638
  * Returns the rows array, or null if any definition is missing complexity
540
639
  * (signalling that JS fallback is needed). */
@@ -543,53 +642,9 @@ function collectNativeBulkRows(
543
642
  fileSymbols: Map<string, FileSymbols>,
544
643
  ): Array<Record<string, unknown>> | null {
545
644
  const rows: Array<Record<string, unknown>> = [];
546
-
547
645
  for (const [relPath, symbols] of fileSymbols) {
548
- const ext = path.extname(relPath).toLowerCase();
549
- const langId = symbols._langId || '';
550
- const langSupported = COMPLEXITY_EXTENSIONS.has(ext) || COMPLEXITY_RULES.has(langId);
551
-
552
- for (const def of symbols.definitions) {
553
- if (def.kind !== 'function' && def.kind !== 'method') continue;
554
- if (!def.line) continue;
555
- // Interface/type property signatures and single-line stubs are extracted
556
- // as methods but the native engine correctly never assigns complexity.
557
- // Mirror the leniency in initWasmParsersIfNeeded to avoid bailing out
558
- // of the native bulk-insert path for every TypeScript codebase (#846).
559
- if (!def.complexity) {
560
- if (def.name.includes('.') || !def.endLine || def.endLine <= def.line) continue;
561
- // Languages without complexity rules will never have data — skip them
562
- // rather than bailing out of the entire native bulk path.
563
- if (!langSupported) continue;
564
- return null; // genuine function body missing complexity — needs JS fallback
565
- }
566
- const nodeId = getFunctionNodeId(db, def.name, relPath, def.line);
567
- if (!nodeId) continue;
568
- const ch = def.complexity.halstead;
569
- const cl = def.complexity.loc;
570
- rows.push({
571
- nodeId,
572
- cognitive: def.complexity.cognitive ?? 0,
573
- cyclomatic: def.complexity.cyclomatic ?? 0,
574
- maxNesting: def.complexity.maxNesting ?? 0,
575
- loc: cl ? cl.loc : 0,
576
- sloc: cl ? cl.sloc : 0,
577
- commentLines: cl ? cl.commentLines : 0,
578
- halsteadN1: ch ? ch.n1 : 0,
579
- halsteadN2: ch ? ch.n2 : 0,
580
- halsteadBigN1: ch ? ch.bigN1 : 0,
581
- halsteadBigN2: ch ? ch.bigN2 : 0,
582
- halsteadVocabulary: ch ? ch.vocabulary : 0,
583
- halsteadLength: ch ? ch.length : 0,
584
- halsteadVolume: ch ? ch.volume : 0,
585
- halsteadDifficulty: ch ? ch.difficulty : 0,
586
- halsteadEffort: ch ? ch.effort : 0,
587
- halsteadBugs: ch ? ch.bugs : 0,
588
- maintainabilityIndex: def.complexity.maintainabilityIndex ?? 0,
589
- });
590
- }
646
+ if (collectFileBulkRows(db, relPath, symbols, rows) === 'fallback') return null;
591
647
  }
592
-
593
648
  return rows;
594
649
  }
595
650
 
@@ -675,6 +675,51 @@ interface BfsParentEntry {
675
675
  expression: string;
676
676
  }
677
677
 
678
+ type DataflowNeighbor = {
679
+ id: number;
680
+ file: string;
681
+ edge_kind: string;
682
+ expression: string;
683
+ };
684
+
685
+ interface DataflowBfsState {
686
+ visited: Set<number>;
687
+ parent: Map<number, BfsParentEntry>;
688
+ nextQueue: number[];
689
+ found: boolean;
690
+ }
691
+
692
+ /**
693
+ * Process a single neighbor in the dataflow BFS. Returns true once the target
694
+ * has been reached so the caller can stop expanding.
695
+ */
696
+ function processDataflowNeighbor(
697
+ n: DataflowNeighbor,
698
+ currentId: number,
699
+ targetId: number,
700
+ noTests: boolean,
701
+ state: DataflowBfsState,
702
+ ): boolean {
703
+ if (noTests && isTestFile(n.file)) return false;
704
+ const entry: BfsParentEntry = {
705
+ parentId: currentId,
706
+ edgeKind: n.edge_kind,
707
+ expression: n.expression,
708
+ };
709
+ if (n.id === targetId) {
710
+ if (!state.found) {
711
+ state.found = true;
712
+ state.parent.set(n.id, entry);
713
+ }
714
+ return true;
715
+ }
716
+ if (state.visited.has(n.id)) return false;
717
+ state.visited.add(n.id);
718
+ state.parent.set(n.id, entry);
719
+ state.nextQueue.push(n.id);
720
+ return false;
721
+ }
722
+
678
723
  /** BFS through dataflow edges to find a path from source to target. */
679
724
  function bfsDataflowPath(
680
725
  db: BetterSqlite3Database,
@@ -689,50 +734,28 @@ function bfsDataflowPath(
689
734
  WHERE d.source_id = ? AND d.kind IN ('flows_to', 'returns')`,
690
735
  );
691
736
 
692
- const visited = new Set<number>([sourceId]);
693
- const parent = new Map<number, BfsParentEntry>();
737
+ const state: DataflowBfsState = {
738
+ visited: new Set<number>([sourceId]),
739
+ parent: new Map<number, BfsParentEntry>(),
740
+ nextQueue: [],
741
+ found: false,
742
+ };
694
743
  let queue = [sourceId];
695
- let found = false;
696
744
 
697
745
  for (let depth = 1; depth <= maxDepth; depth++) {
698
- const nextQueue: number[] = [];
746
+ state.nextQueue = [];
699
747
  for (const currentId of queue) {
700
- const neighbors = neighborStmt.all(currentId) as Array<{
701
- id: number;
702
- file: string;
703
- edge_kind: string;
704
- expression: string;
705
- }>;
748
+ const neighbors = neighborStmt.all(currentId) as DataflowNeighbor[];
706
749
  for (const n of neighbors) {
707
- if (noTests && isTestFile(n.file)) continue;
708
- if (n.id === targetId) {
709
- if (!found) {
710
- found = true;
711
- parent.set(n.id, {
712
- parentId: currentId,
713
- edgeKind: n.edge_kind,
714
- expression: n.expression,
715
- });
716
- }
717
- continue;
718
- }
719
- if (!visited.has(n.id)) {
720
- visited.add(n.id);
721
- parent.set(n.id, {
722
- parentId: currentId,
723
- edgeKind: n.edge_kind,
724
- expression: n.expression,
725
- });
726
- nextQueue.push(n.id);
727
- }
750
+ processDataflowNeighbor(n, currentId, targetId, noTests, state);
728
751
  }
729
752
  }
730
- if (found) break;
731
- queue = nextQueue;
753
+ if (state.found) break;
754
+ queue = state.nextQueue;
732
755
  if (queue.length === 0) break;
733
756
  }
734
757
 
735
- return found ? parent : null;
758
+ return state.found ? state.parent : null;
736
759
  }
737
760
 
738
761
  /** Reconstruct a path from BFS parent map. */
@@ -133,6 +133,41 @@ interface BfsState {
133
133
  truncated: boolean;
134
134
  }
135
135
 
136
+ interface FlowBfsFrame {
137
+ visited: Set<number>;
138
+ cycles: Array<{ from: string; to: string; depth: number }>;
139
+ nodeDepths: Map<number, number>;
140
+ idToNode: Map<number, NodeInfo>;
141
+ nextFrontier: number[];
142
+ levelNodes: NodeInfo[];
143
+ }
144
+
145
+ /** Process one callee row, recording cycle hits or expanding frontier. */
146
+ function processFlowCallee(
147
+ c: CalleeRow,
148
+ fid: number,
149
+ depth: number,
150
+ noTests: boolean,
151
+ frame: FlowBfsFrame,
152
+ ): void {
153
+ if (noTests && isTestFile(c.file)) return;
154
+
155
+ if (frame.visited.has(c.id)) {
156
+ const fromNode = frame.idToNode.get(fid);
157
+ if (fromNode) {
158
+ frame.cycles.push({ from: fromNode.name, to: c.name, depth });
159
+ }
160
+ return;
161
+ }
162
+
163
+ frame.visited.add(c.id);
164
+ frame.nextFrontier.push(c.id);
165
+ const nodeInfo: NodeInfo = toSymbolRef(c);
166
+ frame.levelNodes.push(nodeInfo);
167
+ frame.nodeDepths.set(c.id, depth);
168
+ frame.idToNode.set(c.id, nodeInfo);
169
+ }
170
+
136
171
  /** Forward BFS through callees, collecting steps, cycles, and node depth info. */
137
172
  function bfsCallees(
138
173
  db: ReturnType<typeof openReadonlyOrFail>,
@@ -157,37 +192,26 @@ function bfsCallees(
157
192
  );
158
193
 
159
194
  for (let d = 1; d <= maxDepth; d++) {
160
- const nextFrontier: number[] = [];
161
- const levelNodes: NodeInfo[] = [];
195
+ const frame: FlowBfsFrame = {
196
+ visited,
197
+ cycles,
198
+ nodeDepths,
199
+ idToNode,
200
+ nextFrontier: [],
201
+ levelNodes: [],
202
+ };
162
203
 
163
204
  for (const fid of frontier) {
164
- const callees = calleesStmt.all(fid);
165
-
166
- for (const c of callees) {
167
- if (noTests && isTestFile(c.file)) continue;
168
-
169
- if (visited.has(c.id)) {
170
- const fromNode = idToNode.get(fid);
171
- if (fromNode) {
172
- cycles.push({ from: fromNode.name, to: c.name, depth: d });
173
- }
174
- continue;
175
- }
176
-
177
- visited.add(c.id);
178
- nextFrontier.push(c.id);
179
- const nodeInfo: NodeInfo = toSymbolRef(c);
180
- levelNodes.push(nodeInfo);
181
- nodeDepths.set(c.id, d);
182
- idToNode.set(c.id, nodeInfo);
205
+ for (const c of calleesStmt.all(fid)) {
206
+ processFlowCallee(c, fid, d, noTests, frame);
183
207
  }
184
208
  }
185
209
 
186
- if (levelNodes.length > 0) {
187
- steps.push({ depth: d, nodes: levelNodes });
210
+ if (frame.levelNodes.length > 0) {
211
+ steps.push({ depth: d, nodes: frame.levelNodes });
188
212
  }
189
213
 
190
- frontier = nextFrontier;
214
+ frontier = frame.nextFrontier;
191
215
  if (frontier.length === 0) break;
192
216
  if (d === maxDepth && frontier.length > 0) truncated = true;
193
217
  }