@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
@@ -9,7 +9,7 @@
9
9
  */
10
10
  import fs from 'node:fs';
11
11
  import path from 'node:path';
12
- import { bulkNodeIdsByFile } from '../../../db/index.js';
12
+ import { bulkNodeIdsByFile, purgeFileData } from '../../../db/index.js';
13
13
  import { debug, warn } from '../../../infrastructure/logger.js';
14
14
  import { normalizePath } from '../../../shared/constants.js';
15
15
  import type {
@@ -29,9 +29,8 @@ export interface IncrementalStmts {
29
29
  insertNode: { run: (...params: unknown[]) => unknown };
30
30
  insertEdge: { run: (...params: unknown[]) => unknown };
31
31
  getNodeId: { get: (...params: unknown[]) => { id: number } | undefined };
32
- deleteEdgesForFile: { run: (...params: unknown[]) => unknown };
33
- deleteNodes: { run: (...params: unknown[]) => unknown };
34
32
  countNodes: { get: (...params: unknown[]) => { c: number } | undefined };
33
+ countEdges: { get: (...params: unknown[]) => { c: number } | undefined };
35
34
  listSymbols: { all: (...params: unknown[]) => unknown[] };
36
35
  findNodeInFile: { all: (...params: unknown[]) => unknown[] };
37
36
  findNodeByName: { all: (...params: unknown[]) => unknown[] };
@@ -42,6 +41,7 @@ interface RebuildResult {
42
41
  nodesAdded: number;
43
42
  nodesRemoved: number;
44
43
  edgesAdded: number;
44
+ edgesBefore: number;
45
45
  deleted?: boolean;
46
46
  event?: string;
47
47
  symbolDiff?: unknown;
@@ -208,40 +208,6 @@ function rebuildDirContainment(
208
208
  return 0;
209
209
  }
210
210
 
211
- // ── Ancillary table cleanup ────────────────────────────────────────────
212
-
213
- function purgeAncillaryData(db: BetterSqlite3Database, relPath: string): void {
214
- const tryExec = (sql: string, ...args: string[]): void => {
215
- try {
216
- db.prepare(sql).run(...args);
217
- } catch (err: unknown) {
218
- if (!(err as Error | undefined)?.message?.includes('no such table')) throw err;
219
- }
220
- };
221
- tryExec(
222
- 'DELETE FROM function_complexity WHERE node_id IN (SELECT id FROM nodes WHERE file = ?)',
223
- relPath,
224
- );
225
- tryExec(
226
- 'DELETE FROM node_metrics WHERE node_id IN (SELECT id FROM nodes WHERE file = ?)',
227
- relPath,
228
- );
229
- tryExec(
230
- 'DELETE FROM cfg_edges WHERE function_node_id IN (SELECT id FROM nodes WHERE file = ?)',
231
- relPath,
232
- );
233
- tryExec(
234
- 'DELETE FROM cfg_blocks WHERE function_node_id IN (SELECT id FROM nodes WHERE file = ?)',
235
- relPath,
236
- );
237
- tryExec(
238
- 'DELETE FROM dataflow WHERE source_id IN (SELECT id FROM nodes WHERE file = ?) OR target_id IN (SELECT id FROM nodes WHERE file = ?)',
239
- relPath,
240
- relPath,
241
- );
242
- tryExec('DELETE FROM ast_nodes WHERE file = ?', relPath);
243
- }
244
-
245
211
  // ── Import edge building ────────────────────────────────────────────────
246
212
 
247
213
  // Lazily-cached prepared statements for barrel resolution (avoid re-preparing in hot loops)
@@ -343,6 +309,63 @@ function resolveBarrelImportEdges(
343
309
  return edgesAdded;
344
310
  }
345
311
 
312
+ /** Emit symbol-level `imports-type` edges for a single `import type` statement. */
313
+ function emitTypeOnlySymbolEdges(
314
+ db: BetterSqlite3Database | null,
315
+ stmts: IncrementalStmts,
316
+ imp: ExtractorOutput['imports'][number],
317
+ resolvedPath: string,
318
+ fileNodeId: number,
319
+ ): number {
320
+ let edgesAdded = 0;
321
+ for (const name of imp.names) {
322
+ const cleanName = name.replace(/^\*\s+as\s+/, '');
323
+ let targetFile = resolvedPath;
324
+ if (db && isBarrelFile(db, resolvedPath)) {
325
+ const actual = resolveBarrelTarget(db, resolvedPath, cleanName);
326
+ if (actual) targetFile = actual;
327
+ }
328
+ const candidates = stmts.findNodeInFile.all(cleanName, targetFile) as Array<{
329
+ id: number;
330
+ file: string;
331
+ }>;
332
+ if (candidates.length === 0) continue;
333
+ stmts.insertEdge.run(fileNodeId, candidates[0]!.id, 'imports-type', 1.0, 0);
334
+ edgesAdded++;
335
+ }
336
+ return edgesAdded;
337
+ }
338
+
339
+ /**
340
+ * Process a single import statement: emit the file→file edge, any
341
+ * symbol-level type-only edges, and barrel re-export edges.
342
+ */
343
+ function emitEdgesForImport(
344
+ stmts: IncrementalStmts,
345
+ imp: ExtractorOutput['imports'][number],
346
+ fileNodeId: number,
347
+ relPath: string,
348
+ rootDir: string,
349
+ aliases: PathAliases,
350
+ db: BetterSqlite3Database | null,
351
+ ): number {
352
+ const resolvedPath = resolveImportPath(path.join(rootDir, relPath), imp.source, rootDir, aliases);
353
+ const targetRow = stmts.getNodeId.get(resolvedPath, 'file', resolvedPath, 0);
354
+ if (!targetRow) return 0;
355
+
356
+ const edgeKind = imp.reexport ? 'reexports' : imp.typeOnly ? 'imports-type' : 'imports';
357
+ stmts.insertEdge.run(fileNodeId, targetRow.id, edgeKind, 1.0, 0);
358
+ let edgesAdded = 1;
359
+
360
+ if (imp.typeOnly) {
361
+ edgesAdded += emitTypeOnlySymbolEdges(db, stmts, imp, resolvedPath, fileNodeId);
362
+ }
363
+ if (!imp.reexport && db) {
364
+ edgesAdded += resolveBarrelImportEdges(db, stmts, fileNodeId, resolvedPath, imp);
365
+ }
366
+ return edgesAdded;
367
+ }
368
+
346
369
  function buildImportEdges(
347
370
  stmts: IncrementalStmts,
348
371
  relPath: string,
@@ -354,44 +377,7 @@ function buildImportEdges(
354
377
  ): number {
355
378
  let edgesAdded = 0;
356
379
  for (const imp of symbols.imports) {
357
- const resolvedPath = resolveImportPath(
358
- path.join(rootDir, relPath),
359
- imp.source,
360
- rootDir,
361
- aliases,
362
- );
363
- const targetRow = stmts.getNodeId.get(resolvedPath, 'file', resolvedPath, 0);
364
- if (targetRow) {
365
- const edgeKind = imp.reexport ? 'reexports' : imp.typeOnly ? 'imports-type' : 'imports';
366
- stmts.insertEdge.run(fileNodeId, targetRow.id, edgeKind, 1.0, 0);
367
- edgesAdded++;
368
-
369
- // Type-only imports: create symbol-level edges so the target symbols
370
- // get fan-in credit and aren't falsely classified as dead code.
371
- if (imp.typeOnly) {
372
- for (const name of imp.names) {
373
- const cleanName = name.replace(/^\*\s+as\s+/, '');
374
- let targetFile = resolvedPath;
375
- if (db && isBarrelFile(db, resolvedPath)) {
376
- const actual = resolveBarrelTarget(db, resolvedPath, cleanName);
377
- if (actual) targetFile = actual;
378
- }
379
- const candidates = stmts.findNodeInFile.all(cleanName, targetFile) as Array<{
380
- id: number;
381
- file: string;
382
- }>;
383
- if (candidates.length > 0) {
384
- stmts.insertEdge.run(fileNodeId, candidates[0]!.id, 'imports-type', 1.0, 0);
385
- edgesAdded++;
386
- }
387
- }
388
- }
389
-
390
- // Barrel resolution: create edges through re-export chains
391
- if (!imp.reexport && db) {
392
- edgesAdded += resolveBarrelImportEdges(db, stmts, fileNodeId, resolvedPath, imp);
393
- }
394
- }
380
+ edgesAdded += emitEdgesForImport(stmts, imp, fileNodeId, relPath, rootDir, aliases, db);
395
381
  }
396
382
  return edgesAdded;
397
383
  }
@@ -527,6 +513,139 @@ function buildCallEdges(
527
513
 
528
514
  // ── Main entry point ────────────────────────────────────────────────────
529
515
 
516
+ /** Build the "this file was deleted" result returned by `rebuildFile`. */
517
+ function buildDeletionResult(
518
+ relPath: string,
519
+ oldNodes: number,
520
+ edgesBefore: number,
521
+ oldSymbols: unknown[],
522
+ diffSymbols: ((old: unknown[], new_: unknown[]) => unknown) | undefined,
523
+ ): RebuildResult {
524
+ const symbolDiff = diffSymbols ? diffSymbols(oldSymbols, []) : null;
525
+ return {
526
+ file: relPath,
527
+ nodesAdded: 0,
528
+ nodesRemoved: oldNodes,
529
+ edgesAdded: 0,
530
+ edgesBefore,
531
+ deleted: true,
532
+ event: 'deleted',
533
+ symbolDiff,
534
+ nodesBefore: oldNodes,
535
+ nodesAfter: 0,
536
+ };
537
+ }
538
+
539
+ /** Rebuild all edges originating in the single (just-parsed) target file. */
540
+ function rebuildEdgesForTargetFile(
541
+ db: BetterSqlite3Database,
542
+ stmts: IncrementalStmts,
543
+ relPath: string,
544
+ symbols: ExtractorOutput,
545
+ fileNodeRow: { id: number },
546
+ rootDir: string,
547
+ ): number {
548
+ const aliases: PathAliases = { baseUrl: null, paths: {} };
549
+ let edgesAdded = buildContainmentEdges(db, stmts, relPath, symbols);
550
+ edgesAdded += rebuildDirContainment(db, stmts, relPath);
551
+ edgesAdded += buildImportEdges(stmts, relPath, symbols, rootDir, fileNodeRow.id, aliases, db);
552
+ const importedNames = buildImportedNamesMap(symbols, rootDir, relPath, aliases);
553
+ edgesAdded += buildCallEdges(stmts, relPath, symbols, fileNodeRow, importedNames);
554
+ return edgesAdded;
555
+ }
556
+
557
+ /**
558
+ * Re-parse the reverse-deps and delete their outgoing edges so the cascade
559
+ * can rebuild them. Returns the parsed symbols map together with the total
560
+ * edge count across all deps measured *before* deletion — callers add this
561
+ * to their own `edgesBefore` so the net delta stays correct even when the
562
+ * reverse-dep cascade re-inserts edges.
563
+ */
564
+ async function parseReverseDeps(
565
+ db: BetterSqlite3Database,
566
+ rootDir: string,
567
+ reverseDeps: string[],
568
+ stmts: IncrementalStmts,
569
+ engineOpts: EngineOpts,
570
+ cache: unknown,
571
+ ): Promise<{ depSymbols: Map<string, ExtractorOutput>; reverseDepsEdgesBefore: number }> {
572
+ const depSymbols = new Map<string, ExtractorOutput>();
573
+ let reverseDepsEdgesBefore = 0;
574
+ for (const depRelPath of reverseDeps) {
575
+ const symbols_ = await parseReverseDep(rootDir, depRelPath, engineOpts, cache);
576
+ if (symbols_) {
577
+ reverseDepsEdgesBefore += stmts.countEdges.get(depRelPath)?.c ?? 0;
578
+ deleteOutgoingEdges(db, depRelPath);
579
+ depSymbols.set(depRelPath, symbols_);
580
+ }
581
+ }
582
+ return { depSymbols, reverseDepsEdgesBefore };
583
+ }
584
+
585
+ /**
586
+ * Pass 2 of the reverse-dep cascade: now that the changed file's `reexports`
587
+ * edges exist, resolve barrel imports for every reverse-dep so transitive
588
+ * call edges through the barrel still find their targets.
589
+ */
590
+ function emitBarrelImportEdgesForReverseDeps(
591
+ db: BetterSqlite3Database,
592
+ stmts: IncrementalStmts,
593
+ depSymbols: Map<string, ExtractorOutput>,
594
+ rootDir: string,
595
+ ): number {
596
+ let edgesAdded = 0;
597
+ for (const [depRelPath, symbols_] of depSymbols) {
598
+ const fileNodeRow_ = stmts.getNodeId.get(depRelPath, 'file', depRelPath, 0);
599
+ if (!fileNodeRow_) continue;
600
+ const aliases_: PathAliases = { baseUrl: null, paths: {} };
601
+ for (const imp of symbols_.imports) {
602
+ if (imp.reexport) continue;
603
+ const resolvedPath = resolveImportPath(
604
+ path.join(rootDir, depRelPath),
605
+ imp.source,
606
+ rootDir,
607
+ aliases_,
608
+ );
609
+ edgesAdded += resolveBarrelImportEdges(db, stmts, fileNodeRow_.id, resolvedPath, imp);
610
+ }
611
+ }
612
+ return edgesAdded;
613
+ }
614
+
615
+ /**
616
+ * Two-pass reverse-dep cascade:
617
+ * 1. Rebuild direct edges (creating `reexports` edges for barrels).
618
+ * 2. Add barrel import edges (which need `reexports` edges to exist).
619
+ * Returns both the gross edges-added count and the pre-deletion edge count
620
+ * for all reverse deps so callers can compute a true net delta.
621
+ */
622
+ async function runReverseDepCascade(
623
+ db: BetterSqlite3Database,
624
+ rootDir: string,
625
+ reverseDeps: string[],
626
+ stmts: IncrementalStmts,
627
+ engineOpts: EngineOpts,
628
+ cache: unknown,
629
+ ): Promise<{ edgesAdded: number; reverseDepsEdgesBefore: number }> {
630
+ const { depSymbols, reverseDepsEdgesBefore } = await parseReverseDeps(
631
+ db,
632
+ rootDir,
633
+ reverseDeps,
634
+ stmts,
635
+ engineOpts,
636
+ cache,
637
+ );
638
+
639
+ let edgesAdded = 0;
640
+ // Pass 1: direct edges only (no barrel resolution) — creates reexports edges
641
+ for (const [depRelPath, symbols_] of depSymbols) {
642
+ edgesAdded += rebuildReverseDepEdges(db, rootDir, depRelPath, symbols_, stmts, true);
643
+ }
644
+ // Pass 2: add barrel import edges (reexports edges now exist)
645
+ edgesAdded += emitBarrelImportEdgesForReverseDeps(db, stmts, depSymbols, rootDir);
646
+ return { edgesAdded, reverseDepsEdgesBefore };
647
+ }
648
+
530
649
  /**
531
650
  * Parse a single file and update the database incrementally.
532
651
  */
@@ -542,30 +661,21 @@ export async function rebuildFile(
542
661
  const { diffSymbols } = options;
543
662
  const relPath = normalizePath(path.relative(rootDir, filePath));
544
663
  const oldNodes = stmts.countNodes.get(relPath)?.c || 0;
664
+ const edgesBefore = stmts.countEdges.get(relPath)?.c || 0;
545
665
  const oldSymbols: unknown[] = diffSymbols ? stmts.listSymbols.all(relPath) : [];
546
666
 
547
667
  // Find reverse-deps BEFORE purging (edges still reference the old nodes)
548
668
  const reverseDeps = findReverseDeps(db, relPath);
549
669
 
550
- // Purge ancillary tables, then edges, then nodes
551
- purgeAncillaryData(db, relPath);
552
- stmts.deleteEdgesForFile.run(relPath);
553
- stmts.deleteNodes.run(relPath);
670
+ // Purge ancillary tables (incl. embeddings), edges, and nodes in one pass.
671
+ // Embeddings must be purged before nodes — better-sqlite3 enforces foreign
672
+ // keys by default, and `embeddings.node_id` references `nodes.id`. Issue #1176.
673
+ // `purgeHashes: false` preserves file_hashes for the next incremental build.
674
+ purgeFileData(db, relPath, { purgeHashes: false });
554
675
 
555
676
  if (!fs.existsSync(filePath)) {
556
677
  if (cache) (cache as { remove(p: string): void }).remove(filePath);
557
- const symbolDiff = diffSymbols ? diffSymbols(oldSymbols, []) : null;
558
- return {
559
- file: relPath,
560
- nodesAdded: 0,
561
- nodesRemoved: oldNodes,
562
- edgesAdded: 0,
563
- deleted: true,
564
- event: 'deleted',
565
- symbolDiff,
566
- nodesBefore: oldNodes,
567
- nodesAfter: 0,
568
- };
678
+ return buildDeletionResult(relPath, oldNodes, edgesBefore, oldSymbols, diffSymbols);
569
679
  }
570
680
 
571
681
  let code: string;
@@ -586,47 +696,28 @@ export async function rebuildFile(
586
696
 
587
697
  const fileNodeRow = stmts.getNodeId.get(relPath, 'file', relPath, 0);
588
698
  if (!fileNodeRow)
589
- return { file: relPath, nodesAdded: newNodes, nodesRemoved: oldNodes, edgesAdded: 0 };
590
-
591
- const aliases: PathAliases = { baseUrl: null, paths: {} };
592
-
593
- let edgesAdded = buildContainmentEdges(db, stmts, relPath, symbols);
594
- edgesAdded += rebuildDirContainment(db, stmts, relPath);
595
- edgesAdded += buildImportEdges(stmts, relPath, symbols, rootDir, fileNodeRow.id, aliases, db);
596
- const importedNames = buildImportedNamesMap(symbols, rootDir, relPath, aliases);
597
- edgesAdded += buildCallEdges(stmts, relPath, symbols, fileNodeRow, importedNames);
699
+ return {
700
+ file: relPath,
701
+ nodesAdded: newNodes,
702
+ nodesRemoved: oldNodes,
703
+ edgesAdded: 0,
704
+ edgesBefore,
705
+ };
598
706
 
599
- // Cascade: rebuild outgoing edges for reverse-dep files.
600
- // Two-pass approach: first rebuild direct edges (creating reexports edges for barrels),
601
- // then add barrel import edges (which need reexports edges to exist for resolution).
602
- const depSymbols = new Map<string, ExtractorOutput>();
603
- for (const depRelPath of reverseDeps) {
604
- const symbols_ = await parseReverseDep(rootDir, depRelPath, engineOpts, cache);
605
- if (symbols_) {
606
- deleteOutgoingEdges(db, depRelPath);
607
- depSymbols.set(depRelPath, symbols_);
608
- }
609
- }
610
- // Pass 1: direct edges only (no barrel resolution) creates reexports edges
611
- for (const [depRelPath, symbols_] of depSymbols) {
612
- edgesAdded += rebuildReverseDepEdges(db, rootDir, depRelPath, symbols_, stmts, true);
613
- }
614
- // Pass 2: add barrel import edges (reexports edges now exist)
615
- for (const [depRelPath, symbols_] of depSymbols) {
616
- const fileNodeRow_ = stmts.getNodeId.get(depRelPath, 'file', depRelPath, 0);
617
- if (!fileNodeRow_) continue;
618
- const aliases_: PathAliases = { baseUrl: null, paths: {} };
619
- for (const imp of symbols_.imports) {
620
- if (imp.reexport) continue;
621
- const resolvedPath = resolveImportPath(
622
- path.join(rootDir, depRelPath),
623
- imp.source,
624
- rootDir,
625
- aliases_,
626
- );
627
- edgesAdded += resolveBarrelImportEdges(db, stmts, fileNodeRow_.id, resolvedPath, imp);
628
- }
629
- }
707
+ let edgesAdded = rebuildEdgesForTargetFile(db, stmts, relPath, symbols, fileNodeRow, rootDir);
708
+ const { edgesAdded: cascadeEdges, reverseDepsEdgesBefore } = await runReverseDepCascade(
709
+ db,
710
+ rootDir,
711
+ reverseDeps,
712
+ stmts,
713
+ engineOpts,
714
+ cache,
715
+ );
716
+ edgesAdded += cascadeEdges;
717
+ // Include pre-deletion edge counts from reverse deps so the net delta
718
+ // (edgesAdded - edgesBefore) is correct even when the cascade re-inserts
719
+ // their edges unchanged.
720
+ const totalEdgesBefore = edgesBefore + reverseDepsEdgesBefore;
630
721
 
631
722
  const symbolDiff = diffSymbols ? diffSymbols(oldSymbols, newSymbols) : null;
632
723
  const event = oldNodes === 0 ? 'added' : 'modified';
@@ -636,6 +727,7 @@ export async function rebuildFile(
636
727
  nodesAdded: newNodes,
637
728
  nodesRemoved: oldNodes,
638
729
  edgesAdded,
730
+ edgesBefore: totalEdgesBefore,
639
731
  deleted: false,
640
732
  event,
641
733
  symbolDiff,