@optave/codegraph 3.8.0 → 3.8.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 (296) hide show
  1. package/README.md +9 -8
  2. package/dist/ast-analysis/engine.d.ts.map +1 -1
  3. package/dist/ast-analysis/engine.js +95 -87
  4. package/dist/ast-analysis/engine.js.map +1 -1
  5. package/dist/ast-analysis/metrics.d.ts +0 -3
  6. package/dist/ast-analysis/metrics.d.ts.map +1 -1
  7. package/dist/ast-analysis/metrics.js +30 -13
  8. package/dist/ast-analysis/metrics.js.map +1 -1
  9. package/dist/ast-analysis/shared.d.ts.map +1 -1
  10. package/dist/ast-analysis/shared.js +24 -19
  11. package/dist/ast-analysis/shared.js.map +1 -1
  12. package/dist/ast-analysis/visitor-utils.d.ts.map +1 -1
  13. package/dist/ast-analysis/visitor-utils.js +55 -39
  14. package/dist/ast-analysis/visitor-utils.js.map +1 -1
  15. package/dist/ast-analysis/visitor.d.ts.map +1 -1
  16. package/dist/ast-analysis/visitor.js +91 -70
  17. package/dist/ast-analysis/visitor.js.map +1 -1
  18. package/dist/ast-analysis/visitors/ast-store-visitor.d.ts.map +1 -1
  19. package/dist/ast-analysis/visitors/ast-store-visitor.js +54 -58
  20. package/dist/ast-analysis/visitors/ast-store-visitor.js.map +1 -1
  21. package/dist/ast-analysis/visitors/complexity-visitor.d.ts.map +1 -1
  22. package/dist/ast-analysis/visitors/complexity-visitor.js +32 -39
  23. package/dist/ast-analysis/visitors/complexity-visitor.js.map +1 -1
  24. package/dist/ast-analysis/visitors/dataflow-visitor.d.ts.map +1 -1
  25. package/dist/ast-analysis/visitors/dataflow-visitor.js +57 -38
  26. package/dist/ast-analysis/visitors/dataflow-visitor.js.map +1 -1
  27. package/dist/cli/commands/watch.d.ts.map +1 -1
  28. package/dist/cli/commands/watch.js +16 -2
  29. package/dist/cli/commands/watch.js.map +1 -1
  30. package/dist/db/connection.d.ts.map +1 -1
  31. package/dist/db/connection.js +29 -26
  32. package/dist/db/connection.js.map +1 -1
  33. package/dist/db/query-builder.d.ts.map +1 -1
  34. package/dist/db/query-builder.js +16 -5
  35. package/dist/db/query-builder.js.map +1 -1
  36. package/dist/db/repository/base.d.ts +10 -0
  37. package/dist/db/repository/base.d.ts.map +1 -1
  38. package/dist/db/repository/base.js +17 -0
  39. package/dist/db/repository/base.js.map +1 -1
  40. package/dist/db/repository/native-repository.d.ts +6 -1
  41. package/dist/db/repository/native-repository.d.ts.map +1 -1
  42. package/dist/db/repository/native-repository.js +77 -1
  43. package/dist/db/repository/native-repository.js.map +1 -1
  44. package/dist/db/repository/nodes.d.ts.map +1 -1
  45. package/dist/db/repository/nodes.js +8 -4
  46. package/dist/db/repository/nodes.js.map +1 -1
  47. package/dist/db/repository/sqlite-repository.d.ts +3 -0
  48. package/dist/db/repository/sqlite-repository.d.ts.map +1 -1
  49. package/dist/db/repository/sqlite-repository.js +26 -0
  50. package/dist/db/repository/sqlite-repository.js.map +1 -1
  51. package/dist/domain/analysis/brief.d.ts.map +1 -1
  52. package/dist/domain/analysis/brief.js +13 -17
  53. package/dist/domain/analysis/brief.js.map +1 -1
  54. package/dist/domain/analysis/context.d.ts.map +1 -1
  55. package/dist/domain/analysis/context.js +14 -11
  56. package/dist/domain/analysis/context.js.map +1 -1
  57. package/dist/domain/analysis/dependencies.d.ts.map +1 -1
  58. package/dist/domain/analysis/dependencies.js +53 -52
  59. package/dist/domain/analysis/dependencies.js.map +1 -1
  60. package/dist/domain/analysis/fn-impact.d.ts +2 -7
  61. package/dist/domain/analysis/fn-impact.d.ts.map +1 -1
  62. package/dist/domain/analysis/fn-impact.js +33 -31
  63. package/dist/domain/analysis/fn-impact.js.map +1 -1
  64. package/dist/domain/analysis/implementations.d.ts.map +1 -1
  65. package/dist/domain/analysis/implementations.js +11 -19
  66. package/dist/domain/analysis/implementations.js.map +1 -1
  67. package/dist/domain/analysis/module-map.d.ts.map +1 -1
  68. package/dist/domain/analysis/module-map.js +55 -76
  69. package/dist/domain/analysis/module-map.js.map +1 -1
  70. package/dist/domain/analysis/query-helpers.d.ts +7 -0
  71. package/dist/domain/analysis/query-helpers.d.ts.map +1 -1
  72. package/dist/domain/analysis/query-helpers.js +15 -1
  73. package/dist/domain/analysis/query-helpers.js.map +1 -1
  74. package/dist/domain/graph/builder/pipeline.d.ts.map +1 -1
  75. package/dist/domain/graph/builder/pipeline.js +255 -105
  76. package/dist/domain/graph/builder/pipeline.js.map +1 -1
  77. package/dist/domain/graph/builder/stages/build-edges.d.ts.map +1 -1
  78. package/dist/domain/graph/builder/stages/build-edges.js +22 -17
  79. package/dist/domain/graph/builder/stages/build-edges.js.map +1 -1
  80. package/dist/domain/graph/builder/stages/detect-changes.js +2 -2
  81. package/dist/domain/graph/builder/stages/detect-changes.js.map +1 -1
  82. package/dist/domain/graph/builder/stages/finalize.js +2 -2
  83. package/dist/domain/graph/builder/stages/finalize.js.map +1 -1
  84. package/dist/domain/graph/builder/stages/insert-nodes.d.ts.map +1 -1
  85. package/dist/domain/graph/builder/stages/insert-nodes.js +32 -21
  86. package/dist/domain/graph/builder/stages/insert-nodes.js.map +1 -1
  87. package/dist/domain/graph/builder/stages/resolve-imports.d.ts.map +1 -1
  88. package/dist/domain/graph/builder/stages/resolve-imports.js +95 -84
  89. package/dist/domain/graph/builder/stages/resolve-imports.js.map +1 -1
  90. package/dist/domain/graph/cycles.d.ts +6 -0
  91. package/dist/domain/graph/cycles.d.ts.map +1 -1
  92. package/dist/domain/graph/cycles.js +114 -22
  93. package/dist/domain/graph/cycles.js.map +1 -1
  94. package/dist/domain/graph/resolve.js +1 -1
  95. package/dist/domain/graph/resolve.js.map +1 -1
  96. package/dist/domain/graph/watcher.d.ts +2 -0
  97. package/dist/domain/graph/watcher.d.ts.map +1 -1
  98. package/dist/domain/graph/watcher.js +170 -75
  99. package/dist/domain/graph/watcher.js.map +1 -1
  100. package/dist/domain/parser.d.ts +0 -5
  101. package/dist/domain/parser.d.ts.map +1 -1
  102. package/dist/domain/parser.js +13 -28
  103. package/dist/domain/parser.js.map +1 -1
  104. package/dist/domain/search/generator.js +1 -1
  105. package/dist/domain/search/generator.js.map +1 -1
  106. package/dist/domain/search/models.d.ts +4 -3
  107. package/dist/domain/search/models.d.ts.map +1 -1
  108. package/dist/domain/search/models.js +18 -5
  109. package/dist/domain/search/models.js.map +1 -1
  110. package/dist/domain/search/search/hybrid.d.ts.map +1 -1
  111. package/dist/domain/search/search/hybrid.js +29 -18
  112. package/dist/domain/search/search/hybrid.js.map +1 -1
  113. package/dist/extractors/go.js +36 -33
  114. package/dist/extractors/go.js.map +1 -1
  115. package/dist/extractors/helpers.d.ts.map +1 -1
  116. package/dist/extractors/helpers.js +40 -29
  117. package/dist/extractors/helpers.js.map +1 -1
  118. package/dist/extractors/java.js +58 -46
  119. package/dist/extractors/java.js.map +1 -1
  120. package/dist/extractors/javascript.js +46 -45
  121. package/dist/extractors/javascript.js.map +1 -1
  122. package/dist/extractors/kotlin.js +84 -78
  123. package/dist/extractors/kotlin.js.map +1 -1
  124. package/dist/extractors/python.js +29 -24
  125. package/dist/extractors/python.js.map +1 -1
  126. package/dist/extractors/rust.js +41 -32
  127. package/dist/extractors/rust.js.map +1 -1
  128. package/dist/extractors/solidity.js +58 -67
  129. package/dist/extractors/solidity.js.map +1 -1
  130. package/dist/extractors/swift.js +83 -81
  131. package/dist/extractors/swift.js.map +1 -1
  132. package/dist/extractors/zig.js +58 -60
  133. package/dist/extractors/zig.js.map +1 -1
  134. package/dist/features/ast.d.ts +16 -14
  135. package/dist/features/ast.d.ts.map +1 -1
  136. package/dist/features/ast.js +83 -81
  137. package/dist/features/ast.js.map +1 -1
  138. package/dist/features/audit.d.ts.map +1 -1
  139. package/dist/features/audit.js +8 -6
  140. package/dist/features/audit.js.map +1 -1
  141. package/dist/features/branch-compare.d.ts.map +1 -1
  142. package/dist/features/branch-compare.js +69 -72
  143. package/dist/features/branch-compare.js.map +1 -1
  144. package/dist/features/communities.d.ts.map +1 -1
  145. package/dist/features/communities.js +19 -7
  146. package/dist/features/communities.js.map +1 -1
  147. package/dist/features/complexity.d.ts.map +1 -1
  148. package/dist/features/complexity.js +120 -125
  149. package/dist/features/complexity.js.map +1 -1
  150. package/dist/features/dataflow.d.ts.map +1 -1
  151. package/dist/features/dataflow.js +136 -137
  152. package/dist/features/dataflow.js.map +1 -1
  153. package/dist/features/flow.d.ts.map +1 -1
  154. package/dist/features/flow.js +84 -79
  155. package/dist/features/flow.js.map +1 -1
  156. package/dist/features/structure-query.d.ts.map +1 -1
  157. package/dist/features/structure-query.js +69 -65
  158. package/dist/features/structure-query.js.map +1 -1
  159. package/dist/graph/algorithms/leiden/optimiser.d.ts.map +1 -1
  160. package/dist/graph/algorithms/leiden/optimiser.js +70 -55
  161. package/dist/graph/algorithms/leiden/optimiser.js.map +1 -1
  162. package/dist/graph/algorithms/leiden/partition.d.ts.map +1 -1
  163. package/dist/graph/algorithms/leiden/partition.js +288 -266
  164. package/dist/graph/algorithms/leiden/partition.js.map +1 -1
  165. package/dist/graph/model.d.ts.map +1 -1
  166. package/dist/graph/model.js +5 -1
  167. package/dist/graph/model.js.map +1 -1
  168. package/dist/infrastructure/config.d.ts.map +1 -1
  169. package/dist/infrastructure/config.js +6 -4
  170. package/dist/infrastructure/config.js.map +1 -1
  171. package/dist/infrastructure/suppress.d.ts +25 -0
  172. package/dist/infrastructure/suppress.d.ts.map +1 -0
  173. package/dist/infrastructure/suppress.js +43 -0
  174. package/dist/infrastructure/suppress.js.map +1 -0
  175. package/dist/mcp/server.d.ts.map +1 -1
  176. package/dist/mcp/server.js +29 -24
  177. package/dist/mcp/server.js.map +1 -1
  178. package/dist/presentation/dataflow.d.ts.map +1 -1
  179. package/dist/presentation/dataflow.js +47 -38
  180. package/dist/presentation/dataflow.js.map +1 -1
  181. package/dist/presentation/diff-impact-mermaid.d.ts.map +1 -1
  182. package/dist/presentation/diff-impact-mermaid.js +60 -51
  183. package/dist/presentation/diff-impact-mermaid.js.map +1 -1
  184. package/dist/presentation/queries-cli/exports.d.ts.map +1 -1
  185. package/dist/presentation/queries-cli/exports.js +20 -14
  186. package/dist/presentation/queries-cli/exports.js.map +1 -1
  187. package/dist/presentation/queries-cli/impact.d.ts.map +1 -1
  188. package/dist/presentation/queries-cli/impact.js +15 -13
  189. package/dist/presentation/queries-cli/impact.js.map +1 -1
  190. package/dist/presentation/queries-cli/inspect.d.ts.map +1 -1
  191. package/dist/presentation/queries-cli/inspect.js +101 -79
  192. package/dist/presentation/queries-cli/inspect.js.map +1 -1
  193. package/dist/presentation/queries-cli/overview.d.ts.map +1 -1
  194. package/dist/presentation/queries-cli/overview.js +25 -16
  195. package/dist/presentation/queries-cli/overview.js.map +1 -1
  196. package/dist/presentation/queries-cli/path.js +26 -20
  197. package/dist/presentation/queries-cli/path.js.map +1 -1
  198. package/dist/presentation/result-formatter.d.ts +10 -0
  199. package/dist/presentation/result-formatter.d.ts.map +1 -1
  200. package/dist/presentation/result-formatter.js +16 -1
  201. package/dist/presentation/result-formatter.js.map +1 -1
  202. package/dist/presentation/viewer.d.ts.map +1 -1
  203. package/dist/presentation/viewer.js +18 -12
  204. package/dist/presentation/viewer.js.map +1 -1
  205. package/dist/shared/errors.d.ts +5 -0
  206. package/dist/shared/errors.d.ts.map +1 -1
  207. package/dist/shared/errors.js +5 -0
  208. package/dist/shared/errors.js.map +1 -1
  209. package/dist/shared/hierarchy.d.ts +8 -2
  210. package/dist/shared/hierarchy.d.ts.map +1 -1
  211. package/dist/shared/hierarchy.js +42 -1
  212. package/dist/shared/hierarchy.js.map +1 -1
  213. package/dist/shared/normalize.d.ts +6 -1
  214. package/dist/shared/normalize.d.ts.map +1 -1
  215. package/dist/shared/normalize.js +20 -12
  216. package/dist/shared/normalize.js.map +1 -1
  217. package/dist/shared/paginate.d.ts +0 -9
  218. package/dist/shared/paginate.d.ts.map +1 -1
  219. package/dist/shared/paginate.js +0 -15
  220. package/dist/shared/paginate.js.map +1 -1
  221. package/dist/types.d.ts +10 -4
  222. package/dist/types.d.ts.map +1 -1
  223. package/package.json +7 -7
  224. package/src/ast-analysis/engine.ts +126 -105
  225. package/src/ast-analysis/metrics.ts +33 -11
  226. package/src/ast-analysis/shared.ts +33 -24
  227. package/src/ast-analysis/visitor-utils.ts +52 -32
  228. package/src/ast-analysis/visitor.ts +132 -71
  229. package/src/ast-analysis/visitors/ast-store-visitor.ts +53 -50
  230. package/src/ast-analysis/visitors/complexity-visitor.ts +35 -40
  231. package/src/ast-analysis/visitors/dataflow-visitor.ts +87 -43
  232. package/src/cli/commands/watch.ts +16 -2
  233. package/src/db/connection.ts +29 -28
  234. package/src/db/query-builder.ts +15 -3
  235. package/src/db/repository/base.ts +20 -0
  236. package/src/db/repository/native-repository.ts +79 -1
  237. package/src/db/repository/nodes.ts +13 -8
  238. package/src/db/repository/sqlite-repository.ts +29 -0
  239. package/src/domain/analysis/brief.ts +15 -25
  240. package/src/domain/analysis/context.ts +17 -10
  241. package/src/domain/analysis/dependencies.ts +67 -76
  242. package/src/domain/analysis/fn-impact.ts +36 -43
  243. package/src/domain/analysis/implementations.ts +11 -17
  244. package/src/domain/analysis/module-map.ts +58 -92
  245. package/src/domain/analysis/query-helpers.ts +18 -1
  246. package/src/domain/graph/builder/pipeline.ts +286 -97
  247. package/src/domain/graph/builder/stages/build-edges.ts +22 -18
  248. package/src/domain/graph/builder/stages/detect-changes.ts +2 -2
  249. package/src/domain/graph/builder/stages/finalize.ts +2 -2
  250. package/src/domain/graph/builder/stages/insert-nodes.ts +59 -34
  251. package/src/domain/graph/builder/stages/resolve-imports.ts +122 -100
  252. package/src/domain/graph/cycles.ts +110 -23
  253. package/src/domain/graph/resolve.ts +1 -1
  254. package/src/domain/graph/watcher.ts +202 -96
  255. package/src/domain/parser.ts +14 -26
  256. package/src/domain/search/generator.ts +1 -1
  257. package/src/domain/search/models.ts +17 -4
  258. package/src/domain/search/search/hybrid.ts +69 -51
  259. package/src/extractors/go.ts +43 -33
  260. package/src/extractors/helpers.ts +37 -23
  261. package/src/extractors/java.ts +66 -47
  262. package/src/extractors/javascript.ts +45 -46
  263. package/src/extractors/kotlin.ts +84 -77
  264. package/src/extractors/python.ts +31 -25
  265. package/src/extractors/rust.ts +37 -29
  266. package/src/extractors/solidity.ts +57 -61
  267. package/src/extractors/swift.ts +81 -80
  268. package/src/extractors/zig.ts +58 -61
  269. package/src/features/ast.ts +130 -110
  270. package/src/features/audit.ts +8 -6
  271. package/src/features/branch-compare.ts +105 -79
  272. package/src/features/communities.ts +25 -10
  273. package/src/features/complexity.ts +171 -134
  274. package/src/features/dataflow.ts +165 -175
  275. package/src/features/flow.ts +129 -92
  276. package/src/features/structure-query.ts +79 -64
  277. package/src/graph/algorithms/leiden/optimiser.ts +99 -55
  278. package/src/graph/algorithms/leiden/partition.ts +359 -294
  279. package/src/graph/model.ts +6 -1
  280. package/src/infrastructure/config.ts +6 -4
  281. package/src/infrastructure/suppress.ts +47 -0
  282. package/src/mcp/server.ts +53 -37
  283. package/src/presentation/dataflow.ts +50 -44
  284. package/src/presentation/diff-impact-mermaid.ts +104 -62
  285. package/src/presentation/queries-cli/exports.ts +21 -13
  286. package/src/presentation/queries-cli/impact.ts +15 -13
  287. package/src/presentation/queries-cli/inspect.ts +100 -81
  288. package/src/presentation/queries-cli/overview.ts +26 -16
  289. package/src/presentation/queries-cli/path.ts +33 -25
  290. package/src/presentation/result-formatter.ts +19 -1
  291. package/src/presentation/viewer.ts +42 -14
  292. package/src/shared/errors.ts +6 -0
  293. package/src/shared/hierarchy.ts +50 -2
  294. package/src/shared/normalize.ts +31 -12
  295. package/src/shared/paginate.ts +0 -17
  296. package/src/types.ts +24 -4
@@ -4,7 +4,7 @@ import path from 'node:path';
4
4
  import { fileURLToPath } from 'node:url';
5
5
  import { debug, warn } from '../infrastructure/logger.js';
6
6
  import { getNative, isNativeAvailable } from '../infrastructure/native.js';
7
- import { DbError } from '../shared/errors.js';
7
+ import { DbError, toErrorMessage } from '../shared/errors.js';
8
8
  import type { BetterSqlite3Database, NativeDatabase } from '../types.js';
9
9
  import { getDatabase } from './better-sqlite3.js';
10
10
  import { Repository } from './repository/base.js';
@@ -20,7 +20,8 @@ function getPackageVersion(): string {
20
20
  const pkgPath = path.join(connDir, '..', '..', 'package.json');
21
21
  _packageVersion = (JSON.parse(fs.readFileSync(pkgPath, 'utf-8')) as { version: string })
22
22
  .version;
23
- } catch {
23
+ } catch (e) {
24
+ debug(`Failed to read package version: ${toErrorMessage(e)}`);
24
25
  _packageVersion = '';
25
26
  }
26
27
  return _packageVersion;
@@ -41,8 +42,8 @@ function warnOnVersionMismatch(getBuildVersion: () => string | undefined | null)
41
42
  `DB was built with codegraph v${buildVersion}, running v${currentVersion}. Consider: codegraph build --no-incremental`,
42
43
  );
43
44
  }
44
- } catch {
45
- // build_meta table may not exist in older DBs — silently ignore
45
+ } catch (e) {
46
+ debug(`Version mismatch check skipped (build_meta may not exist): ${toErrorMessage(e)}`);
46
47
  }
47
48
  }
48
49
 
@@ -78,11 +79,11 @@ export function findRepoRoot(fromDir?: string): string | null {
78
79
  try {
79
80
  root = fs.realpathSync(raw);
80
81
  } catch (e) {
81
- debug(`realpathSync failed for git root "${raw}", using resolve: ${(e as Error).message}`);
82
+ debug(`realpathSync failed for git root "${raw}", using resolve: ${toErrorMessage(e)}`);
82
83
  root = path.resolve(raw);
83
84
  }
84
85
  } catch (e) {
85
- debug(`git rev-parse failed for "${dir}": ${(e as Error).message}`);
86
+ debug(`git rev-parse failed for "${dir}": ${toErrorMessage(e)}`);
86
87
  root = null;
87
88
  }
88
89
  if (!fromDir) {
@@ -103,7 +104,7 @@ function isProcessAlive(pid: number): boolean {
103
104
  process.kill(pid, 0);
104
105
  return true;
105
106
  } catch (e) {
106
- debug(`PID ${pid} not alive: ${(e as NodeJS.ErrnoException).code || (e as Error).message}`);
107
+ debug(`PID ${pid} not alive: ${(e as NodeJS.ErrnoException).code || toErrorMessage(e)}`);
107
108
  return false;
108
109
  }
109
110
  }
@@ -119,12 +120,12 @@ function acquireAdvisoryLock(dbPath: string): void {
119
120
  }
120
121
  }
121
122
  } catch (e) {
122
- debug(`Advisory lock read failed: ${(e as Error).message}`);
123
+ debug(`Advisory lock read failed: ${toErrorMessage(e)}`);
123
124
  }
124
125
  try {
125
126
  fs.writeFileSync(lockPath, String(process.pid), 'utf-8');
126
127
  } catch (e) {
127
- debug(`Advisory lock write failed: ${(e as Error).message}`);
128
+ debug(`Advisory lock write failed: ${toErrorMessage(e)}`);
128
129
  }
129
130
  }
130
131
 
@@ -135,7 +136,7 @@ function releaseAdvisoryLock(lockPath: string): void {
135
136
  fs.unlinkSync(lockPath);
136
137
  }
137
138
  } catch (e) {
138
- debug(`Advisory lock release failed for ${lockPath}: ${(e as Error).message}`);
139
+ debug(`Advisory lock release failed for ${lockPath}: ${toErrorMessage(e)}`);
139
140
  }
140
141
  }
141
142
 
@@ -151,7 +152,7 @@ function isSameDirectory(a: string, b: string): boolean {
151
152
  const sb = fs.statSync(b);
152
153
  return sa.dev === sb.dev && sa.ino === sb.ino;
153
154
  } catch (e) {
154
- debug(`isSameDirectory stat failed: ${(e as Error).message}`);
155
+ debug(`isSameDirectory stat failed: ${toErrorMessage(e)}`);
155
156
  return false;
156
157
  }
157
158
  }
@@ -187,8 +188,8 @@ export function flushDeferredClose(): void {
187
188
  const db = _deferredDbs.pop()!;
188
189
  try {
189
190
  db.close();
190
- } catch {
191
- /* ignore handle may already be closed */
191
+ } catch (e) {
192
+ debug(`Deferred DB close failed (handle may already be closed): ${toErrorMessage(e)}`);
192
193
  }
193
194
  }
194
195
  }
@@ -216,8 +217,8 @@ export function closeDbDeferred(db: LockedDatabase): void {
216
217
  _deferredDbs.splice(idx, 1);
217
218
  try {
218
219
  db.close();
219
- } catch {
220
- /* ignore handle may already be closed by flush */
220
+ } catch (e) {
221
+ debug(`Deferred DB close failed (may already be closed by flush): ${toErrorMessage(e)}`);
221
222
  }
222
223
  }
223
224
  });
@@ -239,8 +240,8 @@ export function closeDbPair(pair: LockedDatabasePair): void {
239
240
  if (pair.nativeDb) {
240
241
  try {
241
242
  pair.nativeDb.close();
242
- } catch {
243
- /* ignore */
243
+ } catch (e) {
244
+ debug(`closeDbPair: native close failed: ${toErrorMessage(e)}`);
244
245
  }
245
246
  }
246
247
  closeDb(pair.db);
@@ -251,8 +252,8 @@ export function closeDbPairDeferred(pair: LockedDatabasePair): void {
251
252
  if (pair.nativeDb) {
252
253
  try {
253
254
  pair.nativeDb.close();
254
- } catch {
255
- /* ignore */
255
+ } catch (e) {
256
+ debug(`closeDbPairDeferred: native close failed: ${toErrorMessage(e)}`);
256
257
  }
257
258
  }
258
259
  closeDbDeferred(pair.db);
@@ -270,7 +271,7 @@ export function findDbPath(customPath?: string): string {
270
271
  try {
271
272
  ceiling = fs.realpathSync(rawCeiling);
272
273
  } catch (e) {
273
- debug(`realpathSync failed for ceiling "${rawCeiling}": ${(e as Error).message}`);
274
+ debug(`realpathSync failed for ceiling "${rawCeiling}": ${toErrorMessage(e)}`);
274
275
  ceiling = rawCeiling;
275
276
  }
276
277
  } else {
@@ -281,7 +282,7 @@ export function findDbPath(customPath?: string): string {
281
282
  try {
282
283
  dir = fs.realpathSync(process.cwd());
283
284
  } catch (e) {
284
- debug(`realpathSync failed for cwd: ${(e as Error).message}`);
285
+ debug(`realpathSync failed for cwd: ${toErrorMessage(e)}`);
285
286
  dir = process.cwd();
286
287
  }
287
288
  while (true) {
@@ -334,9 +335,11 @@ function openRepoNative(customDbPath?: string): { repo: Repository; close(): voi
334
335
  const ndb = native.NativeDatabase.openReadonly(dbPath);
335
336
  try {
336
337
  warnOnVersionMismatch(() => ndb.getBuildMeta('codegraph_version'));
338
+ const repo = new NativeRepository(ndb, dbPath);
337
339
  return {
338
- repo: new NativeRepository(ndb),
340
+ repo,
339
341
  close() {
342
+ repo.closeFallback();
340
343
  ndb.close();
341
344
  },
342
345
  };
@@ -375,9 +378,7 @@ export function openRepo(
375
378
  // Re-throw user-visible errors (e.g. DB not found) — only silently
376
379
  // fall back for native-engine failures (e.g. incompatible native binary).
377
380
  if (e instanceof DbError) throw e;
378
- debug(
379
- `openRepo: native path failed, falling back to better-sqlite3: ${(e as Error).message}`,
380
- );
381
+ debug(`openRepo: native path failed, falling back to better-sqlite3: ${toErrorMessage(e)}`);
381
382
  }
382
383
  }
383
384
 
@@ -411,7 +412,7 @@ export function openReadonlyWithNative(customPath?: string): {
411
412
  const native = getNative();
412
413
  nativeDb = native.NativeDatabase.openReadonly(dbPath);
413
414
  } catch (e) {
414
- debug(`openReadonlyWithNative: native path failed: ${(e as Error).message}`);
415
+ debug(`openReadonlyWithNative: native path failed: ${toErrorMessage(e)}`);
415
416
  }
416
417
  }
417
418
 
@@ -423,8 +424,8 @@ export function openReadonlyWithNative(customPath?: string): {
423
424
  if (nativeDb) {
424
425
  try {
425
426
  nativeDb.close();
426
- } catch {
427
- // already closed or not closeable
427
+ } catch (e) {
428
+ debug(`openReadonlyWithNative: native close failed: ${toErrorMessage(e)}`);
428
429
  }
429
430
  }
430
431
  },
@@ -33,14 +33,26 @@ function validateOrderBy(clause: string): void {
33
33
  }
34
34
  }
35
35
 
36
+ /**
37
+ * Track parenthesis depth change for a character.
38
+ * Returns non-zero only for '(' / ')'; a character that is '(' or ')'
39
+ * can never simultaneously be ',' so the comma check in the caller
40
+ * remains mutually exclusive with the depth update.
41
+ */
42
+ function parenDepthDelta(ch: string): number {
43
+ if (ch === '(') return 1;
44
+ if (ch === ')') return -1;
45
+ return 0;
46
+ }
47
+
36
48
  function splitTopLevelCommas(str: string): string[] {
37
49
  const parts: string[] = [];
38
50
  let depth = 0;
39
51
  let start = 0;
40
52
  for (let i = 0; i < str.length; i++) {
41
- if (str[i] === '(') depth++;
42
- else if (str[i] === ')') depth--;
43
- else if (str[i] === ',' && depth === 0) {
53
+ const ch = str[i]!;
54
+ depth += parenDepthDelta(ch);
55
+ if (ch === ',' && depth === 0) {
44
56
  parts.push(str.slice(start, i).trim());
45
57
  start = i + 1;
46
58
  }
@@ -189,4 +189,24 @@ export class Repository implements IRepository {
189
189
  getComplexityForNode(_nodeId: number): ComplexityMetrics | undefined {
190
190
  throw new Error('not implemented');
191
191
  }
192
+
193
+ // ── Convenience queries ──────────────────────────────────────────────
194
+ /**
195
+ * Look up the stored content hash for a file.
196
+ * Returns null when the file is not in file_hashes or the method is
197
+ * not yet implemented on the concrete repository.
198
+ */
199
+ getFileHash(_file: string): string | null {
200
+ return null;
201
+ }
202
+
203
+ /** Check whether the graph contains any 'implements' edges. */
204
+ hasImplementsEdges(): boolean {
205
+ return false;
206
+ }
207
+
208
+ /** Check whether the co_changes table exists and has data. */
209
+ hasCoChangesTable(): boolean {
210
+ return false;
211
+ }
192
212
  }
@@ -8,9 +8,11 @@
8
8
  * back to the snake_case field names that the Repository interface expects.
9
9
  */
10
10
 
11
+ import Database from 'better-sqlite3';
11
12
  import { ConfigError } from '../../shared/errors.js';
12
13
  import type {
13
14
  AdjacentEdgeRow,
15
+ BetterSqlite3Database,
14
16
  CallableNodeRow,
15
17
  CallEdgeRow,
16
18
  ChildNodeRow,
@@ -159,10 +161,33 @@ function toComplexityMetrics(r: NativeComplexityMetrics): ComplexityMetrics {
159
161
 
160
162
  export class NativeRepository extends Repository {
161
163
  #ndb: NativeDatabase;
164
+ #dbPath?: string;
165
+ #fallbackDb?: BetterSqlite3Database;
162
166
 
163
- constructor(ndb: NativeDatabase) {
167
+ constructor(ndb: NativeDatabase, dbPath?: string) {
164
168
  super();
165
169
  this.#ndb = ndb;
170
+ this.#dbPath = dbPath;
171
+ }
172
+
173
+ /** Lazy better-sqlite3 handle for methods not yet ported to Rust. */
174
+ #getFallbackDb(): BetterSqlite3Database | undefined {
175
+ if (this.#fallbackDb) return this.#fallbackDb;
176
+ if (!this.#dbPath) return undefined;
177
+ try {
178
+ this.#fallbackDb = new Database(this.#dbPath, { readonly: true });
179
+ return this.#fallbackDb;
180
+ } catch {
181
+ return undefined;
182
+ }
183
+ }
184
+
185
+ /** Close the lazy fallback connection if it was opened. */
186
+ closeFallback(): void {
187
+ if (this.#fallbackDb) {
188
+ this.#fallbackDb.close();
189
+ this.#fallbackDb = undefined;
190
+ }
166
191
  }
167
192
 
168
193
  // ── Node lookups ──────────────────────────────────────────────────
@@ -358,4 +383,57 @@ export class NativeRepository extends Repository {
358
383
  const r = this.#ndb.getComplexityForNode(nodeId);
359
384
  return r ? toComplexityMetrics(r) : undefined;
360
385
  }
386
+
387
+ // ── Convenience queries ────────────────────────────────────────────
388
+
389
+ getFileHash(file: string): string | null {
390
+ if (typeof this.#ndb.getFileHash === 'function') return this.#ndb.getFileHash(file);
391
+ // Fallback to better-sqlite3 until Rust implements getFileHash
392
+ const db = this.#getFallbackDb();
393
+ if (db) {
394
+ const row = db.prepare('SELECT hash FROM file_hashes WHERE file = ?').get(file) as
395
+ | { hash: string }
396
+ | undefined;
397
+ return row?.hash ?? null;
398
+ }
399
+ return null;
400
+ }
401
+
402
+ #implementsEdgesCache?: boolean;
403
+ hasImplementsEdges(): boolean {
404
+ if (this.#implementsEdgesCache !== undefined) return this.#implementsEdgesCache;
405
+ if (typeof this.#ndb.hasImplementsEdges === 'function') {
406
+ this.#implementsEdgesCache = this.#ndb.hasImplementsEdges();
407
+ return this.#implementsEdgesCache;
408
+ }
409
+ // Fallback to better-sqlite3
410
+ const db = this.#getFallbackDb();
411
+ if (db) {
412
+ this.#implementsEdgesCache = !!db
413
+ .prepare("SELECT 1 FROM edges WHERE kind = 'implements' LIMIT 1")
414
+ .get();
415
+ return this.#implementsEdgesCache;
416
+ }
417
+ return true; // conservative: assume yes when no fallback available
418
+ }
419
+
420
+ #coChangesTableCache?: boolean;
421
+ hasCoChangesTable(): boolean {
422
+ if (this.#coChangesTableCache !== undefined) return this.#coChangesTableCache;
423
+ if (typeof this.#ndb.hasCoChangesTable === 'function') {
424
+ this.#coChangesTableCache = this.#ndb.hasCoChangesTable();
425
+ return this.#coChangesTableCache;
426
+ }
427
+ // Fallback to better-sqlite3
428
+ const db = this.#getFallbackDb();
429
+ if (db) {
430
+ try {
431
+ this.#coChangesTableCache = !!db.prepare('SELECT 1 FROM co_changes LIMIT 1').get();
432
+ } catch {
433
+ this.#coChangesTableCache = false;
434
+ }
435
+ return this.#coChangesTableCache;
436
+ }
437
+ return false;
438
+ }
361
439
  }
@@ -42,14 +42,8 @@ export function findNodesWithFanIn(
42
42
  return q.all(db, nativeDb);
43
43
  }
44
44
 
45
- /**
46
- * Fetch nodes for triage scoring: fan-in + complexity + churn.
47
- */
48
- export function findNodesForTriage(
49
- db: BetterSqlite3Database,
50
- opts: TriageQueryOpts = {},
51
- nativeDb?: NativeDatabase,
52
- ): TriageNodeRow[] {
45
+ /** Validate kind and role options, throwing on invalid values. */
46
+ function validateTriageOpts(opts: TriageQueryOpts): void {
53
47
  if (opts.kind && !(EVERY_SYMBOL_KIND as readonly string[]).includes(opts.kind)) {
54
48
  throw new ConfigError(
55
49
  `Invalid kind: ${opts.kind} (expected one of ${EVERY_SYMBOL_KIND.join(', ')})`,
@@ -58,6 +52,17 @@ export function findNodesForTriage(
58
52
  if (opts.role && !VALID_ROLES.includes(opts.role)) {
59
53
  throw new ConfigError(`Invalid role: ${opts.role} (expected one of ${VALID_ROLES.join(', ')})`);
60
54
  }
55
+ }
56
+
57
+ /**
58
+ * Fetch nodes for triage scoring: fan-in + complexity + churn.
59
+ */
60
+ export function findNodesForTriage(
61
+ db: BetterSqlite3Database,
62
+ opts: TriageQueryOpts = {},
63
+ nativeDb?: NativeDatabase,
64
+ ): TriageNodeRow[] {
65
+ validateTriageOpts(opts);
61
66
 
62
67
  const kindsToUse = opts.kind ? [opts.kind] : ['function', 'method', 'class'];
63
68
  const q = new NodeQuery()
@@ -245,4 +245,33 @@ export class SqliteRepository extends Repository {
245
245
  getComplexityForNode(nodeId: number): ComplexityMetrics | undefined {
246
246
  return getComplexityForNode(this.#db, nodeId);
247
247
  }
248
+
249
+ // ── Convenience queries ────────────────────────────────────────────
250
+
251
+ getFileHash(file: string): string | null {
252
+ const row = this.#db.prepare('SELECT hash FROM file_hashes WHERE file = ?').get(file) as
253
+ | { hash: string }
254
+ | undefined;
255
+ return row?.hash ?? null;
256
+ }
257
+
258
+ #implementsEdgesCache?: boolean;
259
+ hasImplementsEdges(): boolean {
260
+ if (this.#implementsEdgesCache !== undefined) return this.#implementsEdgesCache;
261
+ this.#implementsEdgesCache = !!this.#db
262
+ .prepare("SELECT 1 FROM edges WHERE kind = 'implements' LIMIT 1")
263
+ .get();
264
+ return this.#implementsEdgesCache;
265
+ }
266
+
267
+ #coChangesTableCache?: boolean;
268
+ hasCoChangesTable(): boolean {
269
+ if (this.#coChangesTableCache !== undefined) return this.#coChangesTableCache;
270
+ try {
271
+ this.#coChangesTableCache = !!this.#db.prepare('SELECT 1 FROM co_changes LIMIT 1').get();
272
+ } catch {
273
+ this.#coChangesTableCache = false;
274
+ }
275
+ return this.#coChangesTableCache;
276
+ }
248
277
  }
@@ -1,15 +1,8 @@
1
- import {
2
- findDistinctCallers,
3
- findFileNodes,
4
- findImportDependents,
5
- findImportSources,
6
- findImportTargets,
7
- findNodesByFile,
8
- openReadonlyOrFail,
9
- } from '../../db/index.js';
1
+ import type { Repository } from '../../db/index.js';
10
2
  import { loadConfig } from '../../infrastructure/config.js';
11
3
  import { isTestFile } from '../../infrastructure/test-filter.js';
12
- import type { BetterSqlite3Database, ImportEdgeRow, NodeRow, RelatedNodeRow } from '../../types.js';
4
+ import type { ImportEdgeRow, NodeRow, RelatedNodeRow } from '../../types.js';
5
+ import { withRepo } from './query-helpers.js';
13
6
 
14
7
  /** Symbol kinds meaningful for a file brief — excludes parameters, properties, constants. */
15
8
  const BRIEF_KINDS = new Set([
@@ -49,7 +42,7 @@ function computeRiskTier(
49
42
  * Lightweight variant — only counts, does not collect details.
50
43
  */
51
44
  function countTransitiveCallers(
52
- db: BetterSqlite3Database,
45
+ repo: InstanceType<typeof Repository>,
53
46
  startId: number,
54
47
  noTests: boolean,
55
48
  maxDepth = 5,
@@ -60,7 +53,7 @@ function countTransitiveCallers(
60
53
  for (let d = 1; d <= maxDepth; d++) {
61
54
  const nextFrontier: number[] = [];
62
55
  for (const fid of frontier) {
63
- const callers = findDistinctCallers(db, fid) as RelatedNodeRow[];
56
+ const callers = repo.findDistinctCallers(fid) as RelatedNodeRow[];
64
57
  for (const c of callers) {
65
58
  if (!visited.has(c.id) && (!noTests || !isTestFile(c.file))) {
66
59
  visited.add(c.id);
@@ -80,7 +73,7 @@ function countTransitiveCallers(
80
73
  * Depth-bounded to match countTransitiveCallers and keep hook latency predictable.
81
74
  */
82
75
  function countTransitiveImporters(
83
- db: BetterSqlite3Database,
76
+ repo: InstanceType<typeof Repository>,
84
77
  fileNodeIds: number[],
85
78
  noTests: boolean,
86
79
  maxDepth = 5,
@@ -91,7 +84,7 @@ function countTransitiveImporters(
91
84
  for (let d = 1; d <= maxDepth; d++) {
92
85
  const nextFrontier: number[] = [];
93
86
  for (const current of frontier) {
94
- const dependents = findImportDependents(db, current) as RelatedNodeRow[];
87
+ const dependents = repo.findImportDependents(current) as RelatedNodeRow[];
95
88
  for (const dep of dependents) {
96
89
  if (!visited.has(dep.id) && (!noTests || !isTestFile(dep.file))) {
97
90
  visited.add(dep.id);
@@ -115,38 +108,37 @@ export function briefData(
115
108
  customDbPath: string,
116
109
  opts: { noTests?: boolean; config?: any } = {},
117
110
  ) {
118
- const db = openReadonlyOrFail(customDbPath);
119
- try {
111
+ return withRepo(customDbPath, (repo) => {
120
112
  const noTests = opts.noTests || false;
121
113
  const config = opts.config || loadConfig();
122
114
  const callerDepth = config.analysis?.briefCallerDepth ?? 5;
123
115
  const importerDepth = config.analysis?.briefImporterDepth ?? 5;
124
116
  const highRiskCallers = config.analysis?.briefHighRiskCallers ?? 10;
125
117
  const mediumRiskCallers = config.analysis?.briefMediumRiskCallers ?? 3;
126
- const fileNodes = findFileNodes(db, `%${file}%`) as NodeRow[];
118
+ const fileNodes = repo.findFileNodes(`%${file}%`) as NodeRow[];
127
119
  if (fileNodes.length === 0) {
128
120
  return { file, results: [] };
129
121
  }
130
122
 
131
123
  const results = fileNodes.map((fn) => {
132
124
  // Direct importers
133
- let importedBy = findImportSources(db, fn.id) as ImportEdgeRow[];
125
+ let importedBy = repo.findImportSources(fn.id) as ImportEdgeRow[];
134
126
  if (noTests) importedBy = importedBy.filter((i) => !isTestFile(i.file));
135
127
  const directImporters = [...new Set(importedBy.map((i) => i.file))];
136
128
 
137
129
  // Transitive importer count
138
- const totalImporterCount = countTransitiveImporters(db, [fn.id], noTests, importerDepth);
130
+ const totalImporterCount = countTransitiveImporters(repo, [fn.id], noTests, importerDepth);
139
131
 
140
132
  // Direct imports
141
- let importsTo = findImportTargets(db, fn.id) as ImportEdgeRow[];
133
+ let importsTo = repo.findImportTargets(fn.id) as ImportEdgeRow[];
142
134
  if (noTests) importsTo = importsTo.filter((i) => !isTestFile(i.file));
143
135
 
144
136
  // Symbol definitions with roles and caller counts
145
- const defs = (findNodesByFile(db, fn.file) as NodeRow[]).filter((d) =>
137
+ const defs = (repo.findNodesByFile(fn.file) as NodeRow[]).filter((d) =>
146
138
  BRIEF_KINDS.has(d.kind),
147
139
  );
148
140
  const symbols = defs.map((d) => {
149
- const callerCount = countTransitiveCallers(db, d.id, noTests, callerDepth);
141
+ const callerCount = countTransitiveCallers(repo, d.id, noTests, callerDepth);
150
142
  return {
151
143
  name: d.name,
152
144
  kind: d.kind,
@@ -169,7 +161,5 @@ export function briefData(
169
161
  });
170
162
 
171
163
  return { file, results };
172
- } finally {
173
- db.close();
174
- }
164
+ });
175
165
  }
@@ -264,6 +264,22 @@ function getNodeChildrenSafe(db: BetterSqlite3Database, nodeId: number) {
264
264
  }
265
265
  }
266
266
 
267
+ function buildIntraFileDataFlow(
268
+ db: BetterSqlite3Database,
269
+ file: string,
270
+ ): Array<{ caller: string; callees: string[] }> {
271
+ const intraEdges = findIntraFileCallEdges(db, file) as IntraFileCallEdge[];
272
+ const dataFlowMap = new Map<string, string[]>();
273
+ for (const edge of intraEdges) {
274
+ if (!dataFlowMap.has(edge.caller_name)) dataFlowMap.set(edge.caller_name, []);
275
+ dataFlowMap.get(edge.caller_name)!.push(edge.callee_name);
276
+ }
277
+ return [...dataFlowMap.entries()].map(([caller, callees]) => ({
278
+ caller,
279
+ callees,
280
+ }));
281
+ }
282
+
267
283
  function explainFileImpl(
268
284
  db: BetterSqlite3Database,
269
285
  target: string,
@@ -299,16 +315,7 @@ function explainFileImpl(
299
315
  file: r.file,
300
316
  }));
301
317
 
302
- const intraEdges = findIntraFileCallEdges(db, fn.file) as IntraFileCallEdge[];
303
- const dataFlowMap = new Map<string, string[]>();
304
- for (const edge of intraEdges) {
305
- if (!dataFlowMap.has(edge.caller_name)) dataFlowMap.set(edge.caller_name, []);
306
- dataFlowMap.get(edge.caller_name)!.push(edge.callee_name);
307
- }
308
- const dataFlow = [...dataFlowMap.entries()].map(([caller, callees]) => ({
309
- caller,
310
- callees,
311
- }));
318
+ const dataFlow = buildIntraFileDataFlow(db, fn.file);
312
319
 
313
320
  const metric = getLineCountForNode(db, fn.id) as { line_count: number } | undefined;
314
321
  let lineCount: number | null = metric?.line_count || null;