@optave/codegraph 3.5.0 → 3.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (310) hide show
  1. package/README.md +35 -14
  2. package/dist/ast-analysis/engine.d.ts.map +1 -1
  3. package/dist/ast-analysis/engine.js +119 -127
  4. package/dist/ast-analysis/engine.js.map +1 -1
  5. package/dist/ast-analysis/visitors/ast-store-visitor.d.ts.map +1 -1
  6. package/dist/ast-analysis/visitors/ast-store-visitor.js +14 -1
  7. package/dist/ast-analysis/visitors/ast-store-visitor.js.map +1 -1
  8. package/dist/ast-analysis/visitors/complexity-visitor.d.ts.map +1 -1
  9. package/dist/ast-analysis/visitors/complexity-visitor.js +11 -13
  10. package/dist/ast-analysis/visitors/complexity-visitor.js.map +1 -1
  11. package/dist/db/connection.d.ts +12 -2
  12. package/dist/db/connection.d.ts.map +1 -1
  13. package/dist/db/connection.js +81 -53
  14. package/dist/db/connection.js.map +1 -1
  15. package/dist/db/index.d.ts +1 -1
  16. package/dist/db/index.d.ts.map +1 -1
  17. package/dist/db/index.js +1 -1
  18. package/dist/db/index.js.map +1 -1
  19. package/dist/db/migrations.d.ts.map +1 -1
  20. package/dist/db/migrations.js +38 -32
  21. package/dist/db/migrations.js.map +1 -1
  22. package/dist/domain/analysis/context.d.ts.map +1 -1
  23. package/dist/domain/analysis/context.js +51 -66
  24. package/dist/domain/analysis/context.js.map +1 -1
  25. package/dist/domain/analysis/dependencies.d.ts.map +1 -1
  26. package/dist/domain/analysis/dependencies.js +62 -70
  27. package/dist/domain/analysis/dependencies.js.map +1 -1
  28. package/dist/domain/analysis/diff-impact.d.ts +9 -7
  29. package/dist/domain/analysis/diff-impact.d.ts.map +1 -1
  30. package/dist/domain/analysis/exports.d.ts.map +1 -1
  31. package/dist/domain/analysis/exports.js +29 -33
  32. package/dist/domain/analysis/exports.js.map +1 -1
  33. package/dist/domain/analysis/fn-impact.d.ts +15 -17
  34. package/dist/domain/analysis/fn-impact.d.ts.map +1 -1
  35. package/dist/domain/analysis/fn-impact.js +35 -65
  36. package/dist/domain/analysis/fn-impact.js.map +1 -1
  37. package/dist/domain/analysis/module-map.d.ts.map +1 -1
  38. package/dist/domain/analysis/module-map.js +91 -6
  39. package/dist/domain/analysis/module-map.js.map +1 -1
  40. package/dist/domain/analysis/query-helpers.d.ts +20 -0
  41. package/dist/domain/analysis/query-helpers.d.ts.map +1 -0
  42. package/dist/domain/analysis/query-helpers.js +27 -0
  43. package/dist/domain/analysis/query-helpers.js.map +1 -0
  44. package/dist/domain/graph/builder/helpers.d.ts.map +1 -1
  45. package/dist/domain/graph/builder/helpers.js +15 -9
  46. package/dist/domain/graph/builder/helpers.js.map +1 -1
  47. package/dist/domain/graph/builder/incremental.d.ts.map +1 -1
  48. package/dist/domain/graph/builder/incremental.js +3 -2
  49. package/dist/domain/graph/builder/incremental.js.map +1 -1
  50. package/dist/domain/graph/builder/pipeline.d.ts.map +1 -1
  51. package/dist/domain/graph/builder/pipeline.js +69 -3
  52. package/dist/domain/graph/builder/pipeline.js.map +1 -1
  53. package/dist/domain/graph/builder/stages/build-edges.d.ts.map +1 -1
  54. package/dist/domain/graph/builder/stages/build-edges.js +7 -51
  55. package/dist/domain/graph/builder/stages/build-edges.js.map +1 -1
  56. package/dist/domain/graph/builder/stages/build-structure.d.ts.map +1 -1
  57. package/dist/domain/graph/builder/stages/build-structure.js +7 -5
  58. package/dist/domain/graph/builder/stages/build-structure.js.map +1 -1
  59. package/dist/domain/graph/builder/stages/collect-files.js +2 -2
  60. package/dist/domain/graph/builder/stages/collect-files.js.map +1 -1
  61. package/dist/domain/graph/builder/stages/detect-changes.d.ts.map +1 -1
  62. package/dist/domain/graph/builder/stages/detect-changes.js +2 -2
  63. package/dist/domain/graph/builder/stages/detect-changes.js.map +1 -1
  64. package/dist/domain/graph/builder/stages/finalize.d.ts.map +1 -1
  65. package/dist/domain/graph/builder/stages/finalize.js +124 -105
  66. package/dist/domain/graph/builder/stages/finalize.js.map +1 -1
  67. package/dist/domain/graph/builder/stages/insert-nodes.d.ts.map +1 -1
  68. package/dist/domain/graph/builder/stages/insert-nodes.js +28 -15
  69. package/dist/domain/graph/builder/stages/insert-nodes.js.map +1 -1
  70. package/dist/domain/graph/builder/stages/resolve-imports.d.ts.map +1 -1
  71. package/dist/domain/graph/builder/stages/resolve-imports.js +3 -2
  72. package/dist/domain/graph/builder/stages/resolve-imports.js.map +1 -1
  73. package/dist/domain/graph/resolve.d.ts +0 -4
  74. package/dist/domain/graph/resolve.d.ts.map +1 -1
  75. package/dist/domain/graph/resolve.js +32 -48
  76. package/dist/domain/graph/resolve.js.map +1 -1
  77. package/dist/domain/graph/watcher.d.ts.map +1 -1
  78. package/dist/domain/graph/watcher.js +12 -12
  79. package/dist/domain/graph/watcher.js.map +1 -1
  80. package/dist/domain/parser.d.ts +1 -1
  81. package/dist/domain/parser.d.ts.map +1 -1
  82. package/dist/domain/parser.js +164 -101
  83. package/dist/domain/parser.js.map +1 -1
  84. package/dist/domain/search/search/cli-formatter.d.ts.map +1 -1
  85. package/dist/domain/search/search/cli-formatter.js +88 -83
  86. package/dist/domain/search/search/cli-formatter.js.map +1 -1
  87. package/dist/extractors/bash.d.ts +6 -0
  88. package/dist/extractors/bash.d.ts.map +1 -0
  89. package/dist/extractors/bash.js +91 -0
  90. package/dist/extractors/bash.js.map +1 -0
  91. package/dist/extractors/c.d.ts +6 -0
  92. package/dist/extractors/c.d.ts.map +1 -0
  93. package/dist/extractors/c.js +204 -0
  94. package/dist/extractors/c.js.map +1 -0
  95. package/dist/extractors/cpp.d.ts +6 -0
  96. package/dist/extractors/cpp.d.ts.map +1 -0
  97. package/dist/extractors/cpp.js +283 -0
  98. package/dist/extractors/cpp.js.map +1 -0
  99. package/dist/extractors/csharp.d.ts.map +1 -1
  100. package/dist/extractors/csharp.js +42 -54
  101. package/dist/extractors/csharp.js.map +1 -1
  102. package/dist/extractors/go.d.ts.map +1 -1
  103. package/dist/extractors/go.js +126 -130
  104. package/dist/extractors/go.js.map +1 -1
  105. package/dist/extractors/hcl.js +6 -6
  106. package/dist/extractors/hcl.js.map +1 -1
  107. package/dist/extractors/helpers.d.ts +32 -1
  108. package/dist/extractors/helpers.d.ts.map +1 -1
  109. package/dist/extractors/helpers.js +74 -0
  110. package/dist/extractors/helpers.js.map +1 -1
  111. package/dist/extractors/index.d.ts +6 -0
  112. package/dist/extractors/index.d.ts.map +1 -1
  113. package/dist/extractors/index.js +6 -0
  114. package/dist/extractors/index.js.map +1 -1
  115. package/dist/extractors/java.d.ts.map +1 -1
  116. package/dist/extractors/java.js +32 -47
  117. package/dist/extractors/java.js.map +1 -1
  118. package/dist/extractors/javascript.d.ts.map +1 -1
  119. package/dist/extractors/javascript.js +306 -292
  120. package/dist/extractors/javascript.js.map +1 -1
  121. package/dist/extractors/kotlin.d.ts +6 -0
  122. package/dist/extractors/kotlin.d.ts.map +1 -0
  123. package/dist/extractors/kotlin.js +275 -0
  124. package/dist/extractors/kotlin.js.map +1 -0
  125. package/dist/extractors/php.d.ts.map +1 -1
  126. package/dist/extractors/php.js +39 -44
  127. package/dist/extractors/php.js.map +1 -1
  128. package/dist/extractors/python.d.ts.map +1 -1
  129. package/dist/extractors/python.js +75 -93
  130. package/dist/extractors/python.js.map +1 -1
  131. package/dist/extractors/ruby.js +6 -13
  132. package/dist/extractors/ruby.js.map +1 -1
  133. package/dist/extractors/rust.d.ts.map +1 -1
  134. package/dist/extractors/rust.js +58 -83
  135. package/dist/extractors/rust.js.map +1 -1
  136. package/dist/extractors/scala.d.ts +6 -0
  137. package/dist/extractors/scala.d.ts.map +1 -0
  138. package/dist/extractors/scala.js +269 -0
  139. package/dist/extractors/scala.js.map +1 -0
  140. package/dist/extractors/swift.d.ts +6 -0
  141. package/dist/extractors/swift.d.ts.map +1 -0
  142. package/dist/extractors/swift.js +275 -0
  143. package/dist/extractors/swift.js.map +1 -0
  144. package/dist/features/ast.d.ts +2 -0
  145. package/dist/features/ast.d.ts.map +1 -1
  146. package/dist/features/ast.js +9 -24
  147. package/dist/features/ast.js.map +1 -1
  148. package/dist/features/audit.d.ts.map +1 -1
  149. package/dist/features/audit.js +17 -21
  150. package/dist/features/audit.js.map +1 -1
  151. package/dist/features/branch-compare.d.ts.map +1 -1
  152. package/dist/features/branch-compare.js +47 -3
  153. package/dist/features/branch-compare.js.map +1 -1
  154. package/dist/features/cfg.d.ts +7 -1
  155. package/dist/features/cfg.d.ts.map +1 -1
  156. package/dist/features/cfg.js +118 -62
  157. package/dist/features/cfg.js.map +1 -1
  158. package/dist/features/check.d.ts.map +1 -1
  159. package/dist/features/check.js +79 -62
  160. package/dist/features/check.js.map +1 -1
  161. package/dist/features/complexity-query.d.ts.map +1 -1
  162. package/dist/features/complexity-query.js +142 -137
  163. package/dist/features/complexity-query.js.map +1 -1
  164. package/dist/features/complexity.d.ts +7 -1
  165. package/dist/features/complexity.d.ts.map +1 -1
  166. package/dist/features/complexity.js +62 -1
  167. package/dist/features/complexity.js.map +1 -1
  168. package/dist/features/dataflow.d.ts +7 -1
  169. package/dist/features/dataflow.d.ts.map +1 -1
  170. package/dist/features/dataflow.js +356 -188
  171. package/dist/features/dataflow.js.map +1 -1
  172. package/dist/features/graph-enrichment.d.ts.map +1 -1
  173. package/dist/features/graph-enrichment.js +117 -104
  174. package/dist/features/graph-enrichment.js.map +1 -1
  175. package/dist/features/sequence.d.ts.map +1 -1
  176. package/dist/features/sequence.js +25 -4
  177. package/dist/features/sequence.js.map +1 -1
  178. package/dist/features/structure-query.d.ts.map +1 -1
  179. package/dist/features/structure-query.js +29 -4
  180. package/dist/features/structure-query.js.map +1 -1
  181. package/dist/features/structure.d.ts.map +1 -1
  182. package/dist/features/structure.js +35 -15
  183. package/dist/features/structure.js.map +1 -1
  184. package/dist/graph/algorithms/leiden/adapter.d.ts.map +1 -1
  185. package/dist/graph/algorithms/leiden/adapter.js +88 -73
  186. package/dist/graph/algorithms/leiden/adapter.js.map +1 -1
  187. package/dist/graph/algorithms/leiden/index.js +43 -28
  188. package/dist/graph/algorithms/leiden/index.js.map +1 -1
  189. package/dist/graph/algorithms/leiden/optimiser.d.ts.map +1 -1
  190. package/dist/graph/algorithms/leiden/optimiser.js +90 -104
  191. package/dist/graph/algorithms/leiden/optimiser.js.map +1 -1
  192. package/dist/graph/algorithms/leiden/partition.d.ts.map +1 -1
  193. package/dist/graph/algorithms/leiden/partition.js +89 -106
  194. package/dist/graph/algorithms/leiden/partition.js.map +1 -1
  195. package/dist/graph/model.d.ts +2 -0
  196. package/dist/graph/model.d.ts.map +1 -1
  197. package/dist/graph/model.js +20 -8
  198. package/dist/graph/model.js.map +1 -1
  199. package/dist/infrastructure/config.d.ts +0 -8
  200. package/dist/infrastructure/config.d.ts.map +1 -1
  201. package/dist/infrastructure/config.js +73 -62
  202. package/dist/infrastructure/config.js.map +1 -1
  203. package/dist/infrastructure/registry.d.ts +0 -8
  204. package/dist/infrastructure/registry.d.ts.map +1 -1
  205. package/dist/infrastructure/registry.js +12 -14
  206. package/dist/infrastructure/registry.js.map +1 -1
  207. package/dist/mcp/server.d.ts.map +1 -1
  208. package/dist/mcp/server.js +45 -36
  209. package/dist/mcp/server.js.map +1 -1
  210. package/dist/presentation/audit.d.ts.map +1 -1
  211. package/dist/presentation/audit.js +61 -57
  212. package/dist/presentation/audit.js.map +1 -1
  213. package/dist/presentation/branch-compare.d.ts.map +1 -1
  214. package/dist/presentation/branch-compare.js +56 -38
  215. package/dist/presentation/branch-compare.js.map +1 -1
  216. package/dist/presentation/check.d.ts.map +1 -1
  217. package/dist/presentation/check.js +30 -32
  218. package/dist/presentation/check.js.map +1 -1
  219. package/dist/presentation/colors.d.ts.map +1 -1
  220. package/dist/presentation/colors.js +2 -0
  221. package/dist/presentation/colors.js.map +1 -1
  222. package/dist/presentation/complexity.d.ts.map +1 -1
  223. package/dist/presentation/complexity.js +25 -19
  224. package/dist/presentation/complexity.js.map +1 -1
  225. package/dist/presentation/queries-cli/exports.d.ts.map +1 -1
  226. package/dist/presentation/queries-cli/exports.js +15 -15
  227. package/dist/presentation/queries-cli/exports.js.map +1 -1
  228. package/dist/presentation/queries-cli/impact.d.ts.map +1 -1
  229. package/dist/presentation/queries-cli/impact.js +29 -19
  230. package/dist/presentation/queries-cli/impact.js.map +1 -1
  231. package/dist/types.d.ts +182 -7
  232. package/dist/types.d.ts.map +1 -1
  233. package/grammars/tree-sitter-bash.wasm +0 -0
  234. package/grammars/tree-sitter-c.wasm +0 -0
  235. package/grammars/tree-sitter-cpp.wasm +0 -0
  236. package/grammars/tree-sitter-kotlin.wasm +0 -0
  237. package/grammars/tree-sitter-scala.wasm +0 -0
  238. package/grammars/tree-sitter-swift.wasm +0 -0
  239. package/package.json +13 -7
  240. package/src/ast-analysis/engine.ts +147 -138
  241. package/src/ast-analysis/visitors/ast-store-visitor.ts +15 -2
  242. package/src/ast-analysis/visitors/complexity-visitor.ts +11 -11
  243. package/src/db/connection.ts +90 -59
  244. package/src/db/index.ts +1 -0
  245. package/src/db/migrations.ts +36 -32
  246. package/src/domain/analysis/context.ts +73 -75
  247. package/src/domain/analysis/dependencies.ts +78 -68
  248. package/src/domain/analysis/exports.ts +45 -34
  249. package/src/domain/analysis/fn-impact.ts +67 -64
  250. package/src/domain/analysis/module-map.ts +103 -8
  251. package/src/domain/analysis/query-helpers.ts +35 -0
  252. package/src/domain/graph/builder/helpers.ts +12 -6
  253. package/src/domain/graph/builder/incremental.ts +3 -2
  254. package/src/domain/graph/builder/pipeline.ts +71 -3
  255. package/src/domain/graph/builder/stages/build-edges.ts +10 -75
  256. package/src/domain/graph/builder/stages/build-structure.ts +9 -7
  257. package/src/domain/graph/builder/stages/collect-files.ts +2 -2
  258. package/src/domain/graph/builder/stages/detect-changes.ts +7 -2
  259. package/src/domain/graph/builder/stages/finalize.ts +159 -125
  260. package/src/domain/graph/builder/stages/insert-nodes.ts +32 -21
  261. package/src/domain/graph/builder/stages/resolve-imports.ts +3 -2
  262. package/src/domain/graph/resolve.ts +34 -46
  263. package/src/domain/graph/watcher.ts +12 -14
  264. package/src/domain/parser.ts +168 -97
  265. package/src/domain/search/search/cli-formatter.ts +121 -94
  266. package/src/extractors/bash.ts +97 -0
  267. package/src/extractors/c.ts +212 -0
  268. package/src/extractors/cpp.ts +298 -0
  269. package/src/extractors/csharp.ts +53 -56
  270. package/src/extractors/go.ts +152 -134
  271. package/src/extractors/hcl.ts +6 -6
  272. package/src/extractors/helpers.ts +93 -1
  273. package/src/extractors/index.ts +6 -0
  274. package/src/extractors/java.ts +43 -48
  275. package/src/extractors/javascript.ts +328 -281
  276. package/src/extractors/kotlin.ts +293 -0
  277. package/src/extractors/php.ts +46 -40
  278. package/src/extractors/python.ts +81 -104
  279. package/src/extractors/ruby.ts +6 -13
  280. package/src/extractors/rust.ts +65 -85
  281. package/src/extractors/scala.ts +285 -0
  282. package/src/extractors/swift.ts +293 -0
  283. package/src/features/ast.ts +10 -25
  284. package/src/features/audit.ts +24 -20
  285. package/src/features/branch-compare.ts +51 -4
  286. package/src/features/cfg.ts +158 -65
  287. package/src/features/check.ts +90 -74
  288. package/src/features/complexity-query.ts +181 -163
  289. package/src/features/complexity.ts +64 -1
  290. package/src/features/dataflow.ts +462 -217
  291. package/src/features/graph-enrichment.ts +161 -117
  292. package/src/features/sequence.ts +27 -4
  293. package/src/features/structure-query.ts +43 -4
  294. package/src/features/structure.ts +50 -22
  295. package/src/graph/algorithms/leiden/adapter.ts +126 -71
  296. package/src/graph/algorithms/leiden/index.ts +67 -28
  297. package/src/graph/algorithms/leiden/optimiser.ts +114 -105
  298. package/src/graph/algorithms/leiden/partition.ts +131 -98
  299. package/src/graph/model.ts +19 -7
  300. package/src/infrastructure/config.ts +60 -58
  301. package/src/infrastructure/registry.ts +17 -14
  302. package/src/mcp/server.ts +46 -37
  303. package/src/presentation/audit.ts +72 -67
  304. package/src/presentation/branch-compare.ts +54 -50
  305. package/src/presentation/check.ts +34 -34
  306. package/src/presentation/colors.ts +2 -0
  307. package/src/presentation/complexity.ts +39 -33
  308. package/src/presentation/queries-cli/exports.ts +17 -17
  309. package/src/presentation/queries-cli/impact.ts +30 -22
  310. package/src/types.ts +189 -7
package/src/db/index.ts CHANGED
@@ -11,6 +11,7 @@ export {
11
11
  flushDeferredClose,
12
12
  openDb,
13
13
  openReadonlyOrFail,
14
+ openReadonlyWithNative,
14
15
  openRepo,
15
16
  } from './connection.js';
16
17
  export { getBuildMeta, initSchema, MIGRATIONS, setBuildMeta } from './migrations.js';
@@ -304,7 +304,8 @@ export function setBuildMeta(
304
304
  tx();
305
305
  }
306
306
 
307
- export function initSchema(db: BetterSqlite3Database): void {
307
+ /** Run numbered migrations that haven't been applied yet. */
308
+ function applyMigrations(db: BetterSqlite3Database): void {
308
309
  db.exec(`CREATE TABLE IF NOT EXISTS schema_version (version INTEGER NOT NULL DEFAULT 0)`);
309
310
 
310
311
  const row = db.prepare<{ version: number }>('SELECT version FROM schema_version').get();
@@ -322,40 +323,43 @@ export function initSchema(db: BetterSqlite3Database): void {
322
323
  currentVersion = migration.version;
323
324
  }
324
325
  }
326
+ }
325
327
 
326
- // Legacy column compat — add columns that may be missing from pre-migration DBs
328
+ /** Ensure columns and indexes exist for pre-migration DBs (legacy compat). */
329
+ function ensureLegacyColumns(db: BetterSqlite3Database): void {
327
330
  if (hasTable(db, 'nodes')) {
328
- if (!hasColumn(db, 'nodes', 'end_line')) {
329
- db.exec('ALTER TABLE nodes ADD COLUMN end_line INTEGER');
330
- }
331
- if (!hasColumn(db, 'nodes', 'role')) {
332
- db.exec('ALTER TABLE nodes ADD COLUMN role TEXT');
333
- }
334
- db.exec('CREATE INDEX IF NOT EXISTS idx_nodes_role ON nodes(role)');
335
- if (!hasColumn(db, 'nodes', 'parent_id')) {
336
- db.exec('ALTER TABLE nodes ADD COLUMN parent_id INTEGER REFERENCES nodes(id)');
337
- }
338
- db.exec('CREATE INDEX IF NOT EXISTS idx_nodes_parent ON nodes(parent_id)');
339
- db.exec('CREATE INDEX IF NOT EXISTS idx_nodes_kind_parent ON nodes(kind, parent_id)');
340
- if (!hasColumn(db, 'nodes', 'qualified_name')) {
341
- db.exec('ALTER TABLE nodes ADD COLUMN qualified_name TEXT');
342
- }
343
- if (!hasColumn(db, 'nodes', 'scope')) {
344
- db.exec('ALTER TABLE nodes ADD COLUMN scope TEXT');
345
- }
346
- if (!hasColumn(db, 'nodes', 'visibility')) {
347
- db.exec('ALTER TABLE nodes ADD COLUMN visibility TEXT');
348
- }
349
- db.exec('UPDATE nodes SET qualified_name = name WHERE qualified_name IS NULL');
350
- db.exec('CREATE INDEX IF NOT EXISTS idx_nodes_qualified_name ON nodes(qualified_name)');
351
- db.exec('CREATE INDEX IF NOT EXISTS idx_nodes_scope ON nodes(scope)');
331
+ ensureNodeColumns(db);
352
332
  }
353
333
  if (hasTable(db, 'edges')) {
354
- if (!hasColumn(db, 'edges', 'confidence')) {
355
- db.exec('ALTER TABLE edges ADD COLUMN confidence REAL DEFAULT 1.0');
356
- }
357
- if (!hasColumn(db, 'edges', 'dynamic')) {
358
- db.exec('ALTER TABLE edges ADD COLUMN dynamic INTEGER DEFAULT 0');
359
- }
334
+ ensureEdgeColumns(db);
360
335
  }
361
336
  }
337
+
338
+ function ensureNodeColumns(db: BetterSqlite3Database): void {
339
+ const missing = (col: string) => !hasColumn(db, 'nodes', col);
340
+ if (missing('end_line')) db.exec('ALTER TABLE nodes ADD COLUMN end_line INTEGER');
341
+ if (missing('role')) db.exec('ALTER TABLE nodes ADD COLUMN role TEXT');
342
+ db.exec('CREATE INDEX IF NOT EXISTS idx_nodes_role ON nodes(role)');
343
+ if (missing('parent_id'))
344
+ db.exec('ALTER TABLE nodes ADD COLUMN parent_id INTEGER REFERENCES nodes(id)');
345
+ db.exec('CREATE INDEX IF NOT EXISTS idx_nodes_parent ON nodes(parent_id)');
346
+ db.exec('CREATE INDEX IF NOT EXISTS idx_nodes_kind_parent ON nodes(kind, parent_id)');
347
+ if (missing('qualified_name')) db.exec('ALTER TABLE nodes ADD COLUMN qualified_name TEXT');
348
+ if (missing('scope')) db.exec('ALTER TABLE nodes ADD COLUMN scope TEXT');
349
+ if (missing('visibility')) db.exec('ALTER TABLE nodes ADD COLUMN visibility TEXT');
350
+ db.exec('UPDATE nodes SET qualified_name = name WHERE qualified_name IS NULL');
351
+ db.exec('CREATE INDEX IF NOT EXISTS idx_nodes_qualified_name ON nodes(qualified_name)');
352
+ db.exec('CREATE INDEX IF NOT EXISTS idx_nodes_scope ON nodes(scope)');
353
+ }
354
+
355
+ function ensureEdgeColumns(db: BetterSqlite3Database): void {
356
+ if (!hasColumn(db, 'edges', 'confidence'))
357
+ db.exec('ALTER TABLE edges ADD COLUMN confidence REAL DEFAULT 1.0');
358
+ if (!hasColumn(db, 'edges', 'dynamic'))
359
+ db.exec('ALTER TABLE edges ADD COLUMN dynamic INTEGER DEFAULT 0');
360
+ }
361
+
362
+ export function initSchema(db: BetterSqlite3Database): void {
363
+ applyMigrations(db);
364
+ ensureLegacyColumns(db);
365
+ }
@@ -15,10 +15,8 @@ import {
15
15
  getComplexityForNode,
16
16
  getLineCountForNode,
17
17
  getMaxEndLineForFile,
18
- openReadonlyOrFail,
19
18
  } from '../../db/index.js';
20
19
  import { cachedStmt } from '../../db/repository/cached-stmt.js';
21
- import { loadConfig } from '../../infrastructure/config.js';
22
20
  import { debug } from '../../infrastructure/logger.js';
23
21
  import { isTestFile } from '../../infrastructure/test-filter.js';
24
22
  import {
@@ -40,6 +38,7 @@ import type {
40
38
  RelatedNodeRow,
41
39
  StmtCache,
42
40
  } from '../../types.js';
41
+ import { resolveAnalysisOpts, withReadonlyDb } from './query-helpers.js';
43
42
  import { findMatchingNodes } from './symbol-lookup.js';
44
43
 
45
44
  interface DisplayOpts {
@@ -52,6 +51,60 @@ interface DisplayOpts {
52
51
  [key: string]: unknown;
53
52
  }
54
53
 
54
+ /** Format a callee row into the output shape with summary and source. */
55
+ function formatCalleeRow(
56
+ c: RelatedNodeRow,
57
+ repoRoot: string,
58
+ getFileLines: (file: string) => string[] | null,
59
+ displayOpts: DisplayOpts,
60
+ includeSource: boolean,
61
+ ) {
62
+ const cLines = getFileLines(c.file);
63
+ return {
64
+ name: c.name,
65
+ kind: c.kind,
66
+ file: c.file,
67
+ line: c.line,
68
+ endLine: c.end_line || null,
69
+ summary: cLines ? extractSummary(cLines, c.line, displayOpts) : null,
70
+ source: includeSource
71
+ ? readSourceRange(repoRoot, c.file, c.line, c.end_line ?? undefined, displayOpts)
72
+ : null,
73
+ };
74
+ }
75
+
76
+ /** BFS to collect deeper callees beyond the first level. */
77
+ function collectDeeperCallees(
78
+ db: BetterSqlite3Database,
79
+ startIds: number[],
80
+ rootId: number,
81
+ repoRoot: string,
82
+ getFileLines: (file: string) => string[] | null,
83
+ opts: { noTests: boolean; maxDepth: number; displayOpts: DisplayOpts },
84
+ ) {
85
+ const { noTests, maxDepth, displayOpts } = opts;
86
+ const visited = new Set(startIds);
87
+ visited.add(rootId);
88
+ let frontier = [...startIds];
89
+ const result: ReturnType<typeof formatCalleeRow>[] = [];
90
+
91
+ for (let d = 2; d <= maxDepth; d++) {
92
+ const nextFrontier: number[] = [];
93
+ for (const fid of frontier) {
94
+ const deeper = findCallees(db, fid) as RelatedNodeRow[];
95
+ for (const c of deeper) {
96
+ if (visited.has(c.id) || (noTests && isTestFile(c.file))) continue;
97
+ visited.add(c.id);
98
+ nextFrontier.push(c.id);
99
+ result.push(formatCalleeRow(c, repoRoot, getFileLines, displayOpts, true));
100
+ }
101
+ }
102
+ frontier = nextFrontier;
103
+ if (frontier.length === 0) break;
104
+ }
105
+ return result;
106
+ }
107
+
55
108
  function buildCallees(
56
109
  db: BetterSqlite3Database,
57
110
  node: NodeRow,
@@ -63,65 +116,20 @@ function buildCallees(
63
116
  const calleeRows = findCallees(db, node.id) as RelatedNodeRow[];
64
117
  const filteredCallees = noTests ? calleeRows.filter((c) => !isTestFile(c.file)) : calleeRows;
65
118
 
66
- const callees = filteredCallees.map((c) => {
67
- const cLines = getFileLines(c.file);
68
- const summary = cLines ? extractSummary(cLines, c.line, displayOpts) : null;
69
- let calleeSource: string | null = null;
70
- if (depth >= 1) {
71
- calleeSource = readSourceRange(
72
- repoRoot,
73
- c.file,
74
- c.line,
75
- c.end_line ?? undefined,
76
- displayOpts,
77
- );
78
- }
79
- return {
80
- name: c.name,
81
- kind: c.kind,
82
- file: c.file,
83
- line: c.line,
84
- endLine: c.end_line || null,
85
- summary,
86
- source: calleeSource,
87
- };
88
- });
119
+ const callees = filteredCallees.map((c) =>
120
+ formatCalleeRow(c, repoRoot, getFileLines, displayOpts, depth >= 1),
121
+ );
89
122
 
90
123
  if (depth > 1) {
91
- const visited = new Set(filteredCallees.map((c) => c.id));
92
- visited.add(node.id);
93
- let frontier = filteredCallees.map((c) => c.id);
94
- const maxDepth = Math.min(depth, 5);
95
- for (let d = 2; d <= maxDepth; d++) {
96
- const nextFrontier: number[] = [];
97
- for (const fid of frontier) {
98
- const deeper = findCallees(db, fid) as RelatedNodeRow[];
99
- for (const c of deeper) {
100
- if (!visited.has(c.id) && (!noTests || !isTestFile(c.file))) {
101
- visited.add(c.id);
102
- nextFrontier.push(c.id);
103
- const cLines = getFileLines(c.file);
104
- callees.push({
105
- name: c.name,
106
- kind: c.kind,
107
- file: c.file,
108
- line: c.line,
109
- endLine: c.end_line || null,
110
- summary: cLines ? extractSummary(cLines, c.line, displayOpts) : null,
111
- source: readSourceRange(
112
- repoRoot,
113
- c.file,
114
- c.line,
115
- c.end_line ?? undefined,
116
- displayOpts,
117
- ),
118
- });
119
- }
120
- }
121
- }
122
- frontier = nextFrontier;
123
- if (frontier.length === 0) break;
124
- }
124
+ const deeper = collectDeeperCallees(
125
+ db,
126
+ filteredCallees.map((c) => c.id),
127
+ node.id,
128
+ repoRoot,
129
+ getFileLines,
130
+ { noTests, maxDepth: Math.min(depth, 5), displayOpts },
131
+ );
132
+ callees.push(...deeper);
125
133
  }
126
134
 
127
135
  return callees;
@@ -433,15 +441,12 @@ export function contextData(
433
441
  config?: any;
434
442
  } = {},
435
443
  ) {
436
- const db = openReadonlyOrFail(customDbPath);
437
- try {
444
+ return withReadonlyDb(customDbPath, (db) => {
438
445
  const depth = opts.depth || 0;
439
446
  const noSource = opts.noSource || false;
440
- const noTests = opts.noTests || false;
441
447
  const includeTests = opts.includeTests || false;
442
448
 
443
- const config = opts.config || loadConfig();
444
- const displayOpts: DisplayOpts = config.display || {};
449
+ const { noTests, displayOpts } = resolveAnalysisOpts(opts);
445
450
 
446
451
  const dbPath = findDbPath(customDbPath);
447
452
  const repoRoot = path.resolve(path.dirname(dbPath), '..');
@@ -494,9 +499,7 @@ export function contextData(
494
499
 
495
500
  const base = { name, results };
496
501
  return paginateResult(base, 'results', { limit: opts.limit, offset: opts.offset });
497
- } finally {
498
- db.close();
499
- }
502
+ });
500
503
  }
501
504
 
502
505
  export function explainData(
@@ -510,14 +513,11 @@ export function explainData(
510
513
  config?: any;
511
514
  } = {},
512
515
  ) {
513
- const db = openReadonlyOrFail(customDbPath);
514
- try {
515
- const noTests = opts.noTests || false;
516
+ return withReadonlyDb(customDbPath, (db) => {
516
517
  const depth = opts.depth || 0;
517
518
  const kind = isFileLikeTarget(target) ? 'file' : 'function';
518
519
 
519
- const config = opts.config || loadConfig();
520
- const displayOpts: DisplayOpts = config.display || {};
520
+ const { noTests, displayOpts } = resolveAnalysisOpts(opts);
521
521
 
522
522
  const dbPath = findDbPath(customDbPath);
523
523
  const repoRoot = path.resolve(path.dirname(dbPath), '..');
@@ -536,7 +536,5 @@ export function explainData(
536
536
 
537
537
  const base = { target, kind, results };
538
538
  return paginateResult(base, 'results', { limit: opts.limit, offset: opts.offset });
539
- } finally {
540
- db.close();
541
- }
539
+ });
542
540
  }
@@ -5,7 +5,6 @@ import {
5
5
  findImportSources,
6
6
  findImportTargets,
7
7
  findNodesByFile,
8
- openReadonlyOrFail,
9
8
  } from '../../db/index.js';
10
9
  import { cachedStmt } from '../../db/repository/cached-stmt.js';
11
10
  import { isTestFile } from '../../infrastructure/test-filter.js';
@@ -19,6 +18,7 @@ import type {
19
18
  RelatedNodeRow,
20
19
  StmtCache,
21
20
  } from '../../types.js';
21
+ import { withReadonlyDb } from './query-helpers.js';
22
22
  import { findMatchingNodes } from './symbol-lookup.js';
23
23
 
24
24
  type UpstreamRow = { id: number; name: string; kind: string; file: string; line: number };
@@ -32,8 +32,7 @@ export function fileDepsData(
32
32
  customDbPath: string,
33
33
  opts: { noTests?: boolean; limit?: number; offset?: number } = {},
34
34
  ) {
35
- const db = openReadonlyOrFail(customDbPath);
36
- try {
35
+ return withReadonlyDb(customDbPath, (db) => {
37
36
  const noTests = opts.noTests || false;
38
37
  const fileNodes = findFileNodes(db, `%${file}%`) as NodeRow[];
39
38
  if (fileNodes.length === 0) {
@@ -59,9 +58,7 @@ export function fileDepsData(
59
58
 
60
59
  const base = { file, results };
61
60
  return paginateResult(base, 'results', { limit: opts.limit, offset: opts.offset });
62
- } finally {
63
- db.close();
64
- }
61
+ });
65
62
  }
66
63
 
67
64
  /**
@@ -140,8 +137,7 @@ export function fnDepsData(
140
137
  offset?: number;
141
138
  } = {},
142
139
  ) {
143
- const db = openReadonlyOrFail(customDbPath);
144
- try {
140
+ return withReadonlyDb(customDbPath, (db) => {
145
141
  const depth = opts.depth || 3;
146
142
  const noTests = opts.noTests || false;
147
143
  const hc = new Map();
@@ -194,9 +190,7 @@ export function fnDepsData(
194
190
 
195
191
  const base = { name, results };
196
192
  return paginateResult(base, 'results', { limit: opts.limit, offset: opts.offset });
197
- } finally {
198
- db.close();
199
- }
193
+ });
200
194
  }
201
195
 
202
196
  /**
@@ -384,8 +378,7 @@ export function pathData(
384
378
  kind?: string;
385
379
  } = {},
386
380
  ) {
387
- const db = openReadonlyOrFail(customDbPath);
388
- try {
381
+ return withReadonlyDb(customDbPath, (db) => {
389
382
  const noTests = opts.noTests || false;
390
383
  const maxDepth = opts.maxDepth || 10;
391
384
  const edgeKinds = opts.edgeKinds || ['calls'];
@@ -477,13 +470,67 @@ export function pathData(
477
470
  reverse,
478
471
  maxDepth,
479
472
  };
480
- } finally {
481
- db.close();
482
- }
473
+ });
483
474
  }
484
475
 
485
476
  // ── File-level shortest path ────────────────────────────────────────────
486
477
 
478
+ /** BFS over file adjacency graph to find shortest path. */
479
+ function bfsFilePath(
480
+ neighborStmt: ReturnType<BetterSqlite3Database['prepare']>,
481
+ sourceFile: string,
482
+ targetFile: string,
483
+ edgeKinds: string[],
484
+ maxDepth: number,
485
+ noTests: boolean,
486
+ ): { found: boolean; path: string[]; alternateCount: number } {
487
+ const visited = new Set([sourceFile]);
488
+ const parentMap = new Map<string, string>();
489
+ let queue = [sourceFile];
490
+ let found = false;
491
+ let alternateCount = 0;
492
+
493
+ for (let depth = 1; depth <= maxDepth; depth++) {
494
+ const nextQueue: string[] = [];
495
+ for (const currentFile of queue) {
496
+ const neighbors = neighborStmt.all(currentFile, ...edgeKinds) as Array<{
497
+ neighbor_file: string;
498
+ }>;
499
+ for (const n of neighbors) {
500
+ if (noTests && isTestFile(n.neighbor_file)) continue;
501
+ if (n.neighbor_file === targetFile) {
502
+ if (!found) {
503
+ found = true;
504
+ parentMap.set(n.neighbor_file, currentFile);
505
+ }
506
+ alternateCount++;
507
+ continue;
508
+ }
509
+ if (!visited.has(n.neighbor_file)) {
510
+ visited.add(n.neighbor_file);
511
+ parentMap.set(n.neighbor_file, currentFile);
512
+ nextQueue.push(n.neighbor_file);
513
+ }
514
+ }
515
+ }
516
+ if (found) break;
517
+ queue = nextQueue;
518
+ if (queue.length === 0) break;
519
+ }
520
+
521
+ if (!found) return { found: false, path: [], alternateCount: 0 };
522
+
523
+ // Reconstruct path
524
+ const filePath: string[] = [targetFile];
525
+ let cur = targetFile;
526
+ while (cur !== sourceFile) {
527
+ cur = parentMap.get(cur)!;
528
+ filePath.push(cur);
529
+ }
530
+ filePath.reverse();
531
+ return { found: true, path: filePath, alternateCount: Math.max(0, alternateCount - 1) };
532
+ }
533
+
487
534
  /**
488
535
  * BFS at the file level: find shortest import/edge path between two files.
489
536
  * Adjacency: file A → file B if any symbol in A has an edge to any symbol in B.
@@ -499,8 +546,7 @@ export function filePathData(
499
546
  reverse?: boolean;
500
547
  } = {},
501
548
  ) {
502
- const db = openReadonlyOrFail(customDbPath);
503
- try {
549
+ return withReadonlyDb(customDbPath, (db) => {
504
550
  const noTests = opts.noTests || false;
505
551
  const maxDepth = opts.maxDepth || 10;
506
552
  const edgeKinds = opts.edgeKinds || ['imports', 'imports-type'];
@@ -569,42 +615,17 @@ export function filePathData(
569
615
  WHERE n_src.file = ? AND e.kind IN (${kindPlaceholders}) AND n_tgt.file != n_src.file`;
570
616
  const neighborStmt = db.prepare(neighborQuery);
571
617
 
572
- // BFS
573
- const visited = new Set([sourceFile]);
574
- const parentMap = new Map<string, string>();
575
- let queue = [sourceFile];
576
- let found = false;
577
- let alternateCount = 0;
578
-
579
- for (let depth = 1; depth <= maxDepth; depth++) {
580
- const nextQueue: string[] = [];
581
- for (const currentFile of queue) {
582
- const neighbors = neighborStmt.all(currentFile, ...edgeKinds) as Array<{
583
- neighbor_file: string;
584
- }>;
585
- for (const n of neighbors) {
586
- if (noTests && isTestFile(n.neighbor_file)) continue;
587
- if (n.neighbor_file === targetFile) {
588
- if (!found) {
589
- found = true;
590
- parentMap.set(n.neighbor_file, currentFile);
591
- }
592
- alternateCount++;
593
- continue;
594
- }
595
- if (!visited.has(n.neighbor_file)) {
596
- visited.add(n.neighbor_file);
597
- parentMap.set(n.neighbor_file, currentFile);
598
- nextQueue.push(n.neighbor_file);
599
- }
600
- }
601
- }
602
- if (found) break;
603
- queue = nextQueue;
604
- if (queue.length === 0) break;
605
- }
618
+ // BFS to find shortest file path
619
+ const bfsResult = bfsFilePath(
620
+ neighborStmt,
621
+ sourceFile,
622
+ targetFile,
623
+ edgeKinds,
624
+ maxDepth,
625
+ noTests,
626
+ );
606
627
 
607
- if (!found) {
628
+ if (!bfsResult.found) {
608
629
  return {
609
630
  from,
610
631
  to,
@@ -620,29 +641,18 @@ export function filePathData(
620
641
  };
621
642
  }
622
643
 
623
- // Reconstruct path
624
- const filePath: string[] = [targetFile];
625
- let cur = targetFile;
626
- while (cur !== sourceFile) {
627
- cur = parentMap.get(cur)!;
628
- filePath.push(cur);
629
- }
630
- filePath.reverse();
631
-
632
644
  return {
633
645
  from,
634
646
  to,
635
647
  fromCandidates,
636
648
  toCandidates,
637
649
  found: true,
638
- hops: filePath.length - 1,
639
- path: filePath,
640
- alternateCount: Math.max(0, alternateCount - 1),
650
+ hops: bfsResult.path.length - 1,
651
+ path: bfsResult.path,
652
+ alternateCount: bfsResult.alternateCount,
641
653
  edgeKinds,
642
654
  reverse,
643
655
  maxDepth,
644
656
  };
645
- } finally {
646
- db.close();
647
- }
657
+ });
648
658
  }
@@ -4,10 +4,8 @@ import {
4
4
  findDbPath,
5
5
  findFileNodes,
6
6
  findNodesByFile,
7
- openReadonlyOrFail,
8
7
  } from '../../db/index.js';
9
8
  import { cachedStmt } from '../../db/repository/cached-stmt.js';
10
- import { loadConfig } from '../../infrastructure/config.js';
11
9
  import { debug } from '../../infrastructure/logger.js';
12
10
  import { isTestFile } from '../../infrastructure/test-filter.js';
13
11
  import {
@@ -17,6 +15,7 @@ import {
17
15
  } from '../../shared/file-utils.js';
18
16
  import { paginateResult } from '../../shared/paginate.js';
19
17
  import type { BetterSqlite3Database, NodeRow, StmtCache } from '../../types.js';
18
+ import { resolveAnalysisOpts, withReadonlyDb } from './query-helpers.js';
20
19
 
21
20
  /** Cache the schema probe for the `exported` column per db handle. */
22
21
  const _hasExportedColCache: WeakMap<BetterSqlite3Database, boolean> = new WeakMap();
@@ -37,12 +36,8 @@ export function exportsData(
37
36
  config?: any;
38
37
  } = {},
39
38
  ) {
40
- const db = openReadonlyOrFail(customDbPath);
41
- try {
42
- const noTests = opts.noTests || false;
43
-
44
- const config = opts.config || loadConfig();
45
- const displayOpts = config.display || {};
39
+ return withReadonlyDb(customDbPath, (db) => {
40
+ const { noTests, displayOpts } = resolveAnalysisOpts(opts);
46
41
 
47
42
  const dbFilePath = findDbPath(customDbPath);
48
43
  const repoRoot = path.resolve(path.dirname(dbFilePath), '..');
@@ -101,9 +96,39 @@ export function exportsData(
101
96
  }
102
97
  }
103
98
  return paginated;
104
- } finally {
105
- db.close();
99
+ });
100
+ }
101
+
102
+ /** Collect symbols re-exported through barrel files. */
103
+ function collectReexportedSymbols(
104
+ db: BetterSqlite3Database,
105
+ fileNodeId: number,
106
+ reexportsToStmt: ReturnType<BetterSqlite3Database['prepare']>,
107
+ exportedNodesStmt: ReturnType<BetterSqlite3Database['prepare']> | null,
108
+ hasExportedCol: boolean,
109
+ getFileLines: (file: string) => string[] | null,
110
+ buildSymbolResult: (s: NodeRow, fileLines: string[] | null) => any,
111
+ ) {
112
+ const reexportTargets = reexportsToStmt.all(fileNodeId) as Array<{ file: string }>;
113
+ const reexportedSymbols: Array<ReturnType<typeof buildSymbolResult> & { originFile: string }> =
114
+ [];
115
+ for (const reexTarget of reexportTargets) {
116
+ let targetExported: NodeRow[];
117
+ if (hasExportedCol) {
118
+ targetExported = exportedNodesStmt!.all(reexTarget.file) as NodeRow[];
119
+ } else {
120
+ const targetSymbols = findNodesByFile(db, reexTarget.file) as NodeRow[];
121
+ const exportedIds = findCrossFileCallTargets(db, reexTarget.file) as Set<number>;
122
+ targetExported = targetSymbols.filter((s) => exportedIds.has(s.id));
123
+ }
124
+ for (const s of targetExported) {
125
+ reexportedSymbols.push({
126
+ ...buildSymbolResult(s, getFileLines(reexTarget.file)),
127
+ originFile: reexTarget.file,
128
+ });
129
+ }
106
130
  }
131
+ return reexportedSymbols;
107
132
  }
108
133
 
109
134
  function exportsFileImpl(
@@ -197,34 +222,20 @@ function exportsFileImpl(
197
222
 
198
223
  const totalUnused = results.filter((r) => r.consumerCount === 0).length;
199
224
 
200
- // Files that re-export this file (barrel -> this file)
201
225
  const reexports = (reexportsFromStmt.all(fn.id) as Array<{ file: string }>).map((r) => ({
202
226
  file: r.file,
203
227
  }));
204
228
 
205
- // For barrel files: gather symbols re-exported from target modules
206
- const reexportTargets = reexportsToStmt.all(fn.id) as Array<{ file: string }>;
207
-
208
- const reexportedSymbols: Array<ReturnType<typeof buildSymbolResult> & { originFile: string }> =
209
- [];
210
- for (const reexTarget of reexportTargets) {
211
- let targetExported: NodeRow[];
212
- if (hasExportedCol) {
213
- targetExported = exportedNodesStmt!.all(reexTarget.file) as NodeRow[];
214
- } else {
215
- // Fallback: same heuristic as direct exports — symbols called from other files
216
- const targetSymbols = findNodesByFile(db, reexTarget.file) as NodeRow[];
217
- const exportedIds = findCrossFileCallTargets(db, reexTarget.file) as Set<number>;
218
- targetExported = targetSymbols.filter((s) => exportedIds.has(s.id));
219
- }
220
- for (const s of targetExported) {
221
- const fileLines = getFileLines(reexTarget.file);
222
- reexportedSymbols.push({
223
- ...buildSymbolResult(s, fileLines),
224
- originFile: reexTarget.file,
225
- });
226
- }
227
- }
229
+ // Gather symbols re-exported from target modules (barrel file support)
230
+ const reexportedSymbols = collectReexportedSymbols(
231
+ db,
232
+ fn.id,
233
+ reexportsToStmt,
234
+ exportedNodesStmt,
235
+ hasExportedCol,
236
+ getFileLines,
237
+ buildSymbolResult,
238
+ );
228
239
 
229
240
  let filteredResults = results;
230
241
  let filteredReexported = reexportedSymbols;