@optave/codegraph 3.4.1 → 3.5.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 (112) hide show
  1. package/README.md +22 -21
  2. package/dist/ast-analysis/rules/javascript.d.ts.map +1 -1
  3. package/dist/ast-analysis/rules/javascript.js +1 -0
  4. package/dist/ast-analysis/rules/javascript.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 +103 -35
  7. package/dist/ast-analysis/visitors/ast-store-visitor.js.map +1 -1
  8. package/dist/db/better-sqlite3.d.ts +3 -0
  9. package/dist/db/better-sqlite3.d.ts.map +1 -0
  10. package/dist/db/better-sqlite3.js +19 -0
  11. package/dist/db/better-sqlite3.js.map +1 -0
  12. package/dist/db/connection.d.ts +13 -2
  13. package/dist/db/connection.d.ts.map +1 -1
  14. package/dist/db/connection.js +76 -2
  15. package/dist/db/connection.js.map +1 -1
  16. package/dist/db/index.d.ts +2 -2
  17. package/dist/db/index.d.ts.map +1 -1
  18. package/dist/db/index.js +1 -1
  19. package/dist/db/index.js.map +1 -1
  20. package/dist/db/migrations.d.ts.map +1 -1
  21. package/dist/db/migrations.js +2 -0
  22. package/dist/db/migrations.js.map +1 -1
  23. package/dist/db/query-builder.d.ts +5 -5
  24. package/dist/db/query-builder.d.ts.map +1 -1
  25. package/dist/db/query-builder.js +20 -4
  26. package/dist/db/query-builder.js.map +1 -1
  27. package/dist/db/repository/index.d.ts +1 -0
  28. package/dist/db/repository/index.d.ts.map +1 -1
  29. package/dist/db/repository/index.js +1 -0
  30. package/dist/db/repository/index.js.map +1 -1
  31. package/dist/db/repository/native-repository.d.ts +58 -0
  32. package/dist/db/repository/native-repository.d.ts.map +1 -0
  33. package/dist/db/repository/native-repository.js +261 -0
  34. package/dist/db/repository/native-repository.js.map +1 -0
  35. package/dist/db/repository/nodes.d.ts +4 -4
  36. package/dist/db/repository/nodes.d.ts.map +1 -1
  37. package/dist/db/repository/nodes.js +6 -6
  38. package/dist/db/repository/nodes.js.map +1 -1
  39. package/dist/domain/graph/builder/context.d.ts +2 -1
  40. package/dist/domain/graph/builder/context.d.ts.map +1 -1
  41. package/dist/domain/graph/builder/context.js +1 -0
  42. package/dist/domain/graph/builder/context.js.map +1 -1
  43. package/dist/domain/graph/builder/pipeline.d.ts.map +1 -1
  44. package/dist/domain/graph/builder/pipeline.js +29 -7
  45. package/dist/domain/graph/builder/pipeline.js.map +1 -1
  46. package/dist/domain/graph/builder/stages/build-edges.d.ts.map +1 -1
  47. package/dist/domain/graph/builder/stages/build-edges.js +103 -15
  48. package/dist/domain/graph/builder/stages/build-edges.js.map +1 -1
  49. package/dist/domain/graph/builder/stages/build-structure.d.ts.map +1 -1
  50. package/dist/domain/graph/builder/stages/build-structure.js +33 -5
  51. package/dist/domain/graph/builder/stages/build-structure.js.map +1 -1
  52. package/dist/domain/graph/builder/stages/collect-files.d.ts.map +1 -1
  53. package/dist/domain/graph/builder/stages/collect-files.js +71 -7
  54. package/dist/domain/graph/builder/stages/collect-files.js.map +1 -1
  55. package/dist/domain/graph/builder/stages/detect-changes.d.ts.map +1 -1
  56. package/dist/domain/graph/builder/stages/detect-changes.js +36 -14
  57. package/dist/domain/graph/builder/stages/detect-changes.js.map +1 -1
  58. package/dist/domain/graph/builder/stages/finalize.d.ts.map +1 -1
  59. package/dist/domain/graph/builder/stages/finalize.js +43 -20
  60. package/dist/domain/graph/builder/stages/finalize.js.map +1 -1
  61. package/dist/domain/graph/builder/stages/insert-nodes.d.ts.map +1 -1
  62. package/dist/domain/graph/builder/stages/insert-nodes.js +104 -9
  63. package/dist/domain/graph/builder/stages/insert-nodes.js.map +1 -1
  64. package/dist/domain/parser.d.ts.map +1 -1
  65. package/dist/domain/parser.js +1 -0
  66. package/dist/domain/parser.js.map +1 -1
  67. package/dist/extractors/javascript.js +53 -38
  68. package/dist/extractors/javascript.js.map +1 -1
  69. package/dist/extractors/rust.js +2 -1
  70. package/dist/extractors/rust.js.map +1 -1
  71. package/dist/features/ast.d.ts +14 -1
  72. package/dist/features/ast.d.ts.map +1 -1
  73. package/dist/features/ast.js +38 -1
  74. package/dist/features/ast.js.map +1 -1
  75. package/dist/features/branch-compare.d.ts.map +1 -1
  76. package/dist/features/branch-compare.js +3 -1
  77. package/dist/features/branch-compare.js.map +1 -1
  78. package/dist/features/snapshot.d.ts.map +1 -1
  79. package/dist/features/snapshot.js +2 -1
  80. package/dist/features/snapshot.js.map +1 -1
  81. package/dist/mcp/server.d.ts.map +1 -1
  82. package/dist/mcp/server.js +2 -9
  83. package/dist/mcp/server.js.map +1 -1
  84. package/dist/types.d.ts +228 -0
  85. package/dist/types.d.ts.map +1 -1
  86. package/package.json +61 -11
  87. package/src/ast-analysis/rules/javascript.ts +1 -0
  88. package/src/ast-analysis/visitors/ast-store-visitor.ts +102 -33
  89. package/src/db/better-sqlite3.ts +20 -0
  90. package/src/db/connection.ts +94 -3
  91. package/src/db/index.ts +3 -1
  92. package/src/db/migrations.ts +2 -0
  93. package/src/db/query-builder.ts +30 -5
  94. package/src/db/repository/index.ts +1 -0
  95. package/src/db/repository/native-repository.ts +361 -0
  96. package/src/db/repository/nodes.ts +7 -3
  97. package/src/domain/graph/builder/context.ts +2 -0
  98. package/src/domain/graph/builder/pipeline.ts +30 -6
  99. package/src/domain/graph/builder/stages/build-edges.ts +117 -19
  100. package/src/domain/graph/builder/stages/build-structure.ts +47 -11
  101. package/src/domain/graph/builder/stages/collect-files.ts +84 -7
  102. package/src/domain/graph/builder/stages/detect-changes.ts +39 -21
  103. package/src/domain/graph/builder/stages/finalize.ts +49 -20
  104. package/src/domain/graph/builder/stages/insert-nodes.ts +129 -10
  105. package/src/domain/parser.ts +1 -0
  106. package/src/extractors/javascript.ts +54 -36
  107. package/src/extractors/rust.ts +2 -1
  108. package/src/features/ast.ts +66 -1
  109. package/src/features/branch-compare.ts +3 -1
  110. package/src/features/snapshot.ts +2 -1
  111. package/src/mcp/server.ts +2 -10
  112. package/src/types.ts +273 -0
package/src/db/index.ts CHANGED
@@ -1,9 +1,11 @@
1
1
  // Barrel re-export — keeps all existing `import { ... } from '…/db/index.js'` working.
2
2
 
3
- export type { LockedDatabase } from './connection.js';
3
+ export type { LockedDatabase, LockedDatabasePair } from './connection.js';
4
4
  export {
5
5
  closeDb,
6
6
  closeDbDeferred,
7
+ closeDbPair,
8
+ closeDbPairDeferred,
7
9
  findDbPath,
8
10
  findRepoRoot,
9
11
  flushDeferredClose,
@@ -8,6 +8,8 @@ interface Migration {
8
8
  up: string;
9
9
  }
10
10
 
11
+ // IMPORTANT: Migration DDL is mirrored in crates/codegraph-core/src/native_db.rs.
12
+ // Any changes here MUST be reflected there (and vice-versa).
11
13
  export const MIGRATIONS: Migration[] = [
12
14
  {
13
15
  version: 1,
@@ -1,6 +1,6 @@
1
1
  import { DbError } from '../shared/errors.js';
2
2
  import { DEAD_ROLE_PREFIX, EVERY_EDGE_KIND } from '../shared/kinds.js';
3
- import type { BetterSqlite3Database } from '../types.js';
3
+ import type { BetterSqlite3Database, NativeDatabase } from '../types.js';
4
4
 
5
5
  // ─── Validation Helpers ─────────────────────────────────────────────
6
6
 
@@ -66,6 +66,17 @@ function validateEdgeKind(edgeKind: string): void {
66
66
  }
67
67
  }
68
68
 
69
+ /** Runtime-validate that every param is string, number, or null before sending to nativeDb. */
70
+ function validateNativeParams(params: (string | number)[]): Array<string | number | null> {
71
+ for (let i = 0; i < params.length; i++) {
72
+ const p = params[i];
73
+ if (p !== null && typeof p !== 'string' && typeof p !== 'number') {
74
+ throw new DbError(`NodeQuery param[${i}] has unsupported type: ${typeof p}`);
75
+ }
76
+ }
77
+ return params as Array<string | number | null>;
78
+ }
79
+
69
80
  // ─── LIKE Escaping ──────────────────────────────────────────────────
70
81
 
71
82
  /** Escape LIKE wildcards in a literal string segment. */
@@ -314,15 +325,29 @@ export class NodeQuery {
314
325
  return { sql, params };
315
326
  }
316
327
 
317
- /** Execute and return all rows. */
318
- all<TRow = Record<string, unknown>>(db: BetterSqlite3Database): TRow[] {
328
+ /** Execute and return all rows. When `nativeDb` is provided, dispatches through rusqlite. */
329
+ all<TRow = Record<string, unknown>>(
330
+ db: BetterSqlite3Database,
331
+ nativeDb?: NativeDatabase,
332
+ ): TRow[] {
319
333
  const { sql, params } = this.build();
334
+ if (nativeDb) {
335
+ return nativeDb.queryAll(sql, validateNativeParams(params)) as TRow[];
336
+ }
320
337
  return db.prepare<TRow>(sql).all(...params) as TRow[];
321
338
  }
322
339
 
323
- /** Execute and return first row. */
324
- get<TRow = Record<string, unknown>>(db: BetterSqlite3Database): TRow | undefined {
340
+ /** Execute and return first row. When `nativeDb` is provided, dispatches through rusqlite. */
341
+ get<TRow = Record<string, unknown>>(
342
+ db: BetterSqlite3Database,
343
+ nativeDb?: NativeDatabase,
344
+ ): TRow | undefined {
325
345
  const { sql, params } = this.build();
346
+ if (nativeDb) {
347
+ return (nativeDb.queryGet(sql, validateNativeParams(params)) ?? undefined) as
348
+ | TRow
349
+ | undefined;
350
+ }
326
351
  return db.prepare<TRow>(sql).get(...params) as TRow | undefined;
327
352
  }
328
353
 
@@ -28,6 +28,7 @@ export {
28
28
  export { getEmbeddingCount, getEmbeddingMeta, hasEmbeddings } from './embeddings.js';
29
29
  export { getCallableNodes, getCallEdges, getFileNodesAll, getImportEdges } from './graph-read.js';
30
30
  export { InMemoryRepository } from './in-memory-repository.js';
31
+ export { NativeRepository } from './native-repository.js';
31
32
  export {
32
33
  bulkNodeIdsByFile,
33
34
  countEdges,
@@ -0,0 +1,361 @@
1
+ /**
2
+ * NativeRepository — delegates all Repository read methods to NativeDatabase (rusqlite via napi-rs).
3
+ *
4
+ * Phase 6.14: every query runs via rusqlite when the native engine is available.
5
+ * Falls back to SqliteRepository (better-sqlite3) when native is unavailable.
6
+ *
7
+ * napi-rs converts Rust snake_case fields to JS camelCase. This class maps them
8
+ * back to the snake_case field names that the Repository interface expects.
9
+ */
10
+
11
+ import { ConfigError } from '../../shared/errors.js';
12
+ import type {
13
+ AdjacentEdgeRow,
14
+ CallableNodeRow,
15
+ CallEdgeRow,
16
+ ChildNodeRow,
17
+ ComplexityMetrics,
18
+ FileNodeRow,
19
+ ImportEdgeRow,
20
+ ImportGraphEdgeRow,
21
+ IntraFileCallEdge,
22
+ ListFunctionOpts,
23
+ NativeAdjacentEdgeRow,
24
+ NativeCallableNodeRow,
25
+ NativeCallEdgeRow,
26
+ NativeChildNodeRow,
27
+ NativeComplexityMetrics,
28
+ NativeDatabase,
29
+ NativeFileNodeRow,
30
+ NativeImportEdgeRow,
31
+ NativeImportGraphEdgeRow,
32
+ NativeIntraFileCallEdge,
33
+ NativeNodeIdRow,
34
+ NativeNodeRow,
35
+ NativeNodeRowWithFanIn,
36
+ NativeRelatedNodeRow,
37
+ NativeTriageNodeRow,
38
+ NodeIdRow,
39
+ NodeRow,
40
+ NodeRowWithFanIn,
41
+ QueryOpts,
42
+ RelatedNodeRow,
43
+ TriageNodeRow,
44
+ TriageQueryOpts,
45
+ } from '../../types.js';
46
+ import { Repository } from './base.js';
47
+
48
+ // ── Row converters (napi camelCase → Repository snake_case) ─────────────
49
+
50
+ function toNodeRow(r: NativeNodeRow): NodeRow {
51
+ return {
52
+ id: r.id,
53
+ name: r.name,
54
+ kind: r.kind as NodeRow['kind'],
55
+ file: r.file,
56
+ line: r.line ?? 0,
57
+ end_line: r.endLine ?? null,
58
+ parent_id: r.parentId ?? null,
59
+ exported: (r.exported ?? null) as 0 | 1 | null,
60
+ qualified_name: r.qualifiedName ?? null,
61
+ scope: r.scope ?? null,
62
+ visibility: (r.visibility ?? null) as NodeRow['visibility'],
63
+ role: (r.role ?? null) as NodeRow['role'],
64
+ };
65
+ }
66
+
67
+ function toNodeRowWithFanIn(r: NativeNodeRowWithFanIn): NodeRowWithFanIn {
68
+ return { ...toNodeRow(r), fan_in: r.fanIn };
69
+ }
70
+
71
+ function toTriageNodeRow(r: NativeTriageNodeRow): TriageNodeRow {
72
+ return {
73
+ ...toNodeRow(r),
74
+ fan_in: r.fanIn,
75
+ cognitive: r.cognitive,
76
+ mi: r.mi,
77
+ cyclomatic: r.cyclomatic,
78
+ max_nesting: r.maxNesting,
79
+ churn: r.churn,
80
+ };
81
+ }
82
+
83
+ function toNodeIdRow(r: NativeNodeIdRow): NodeIdRow {
84
+ return { id: r.id, name: r.name, kind: r.kind, line: r.line ?? 0 };
85
+ }
86
+
87
+ function toChildNodeRow(r: NativeChildNodeRow): ChildNodeRow {
88
+ return {
89
+ name: r.name,
90
+ kind: r.kind as ChildNodeRow['kind'],
91
+ line: r.line ?? 0,
92
+ end_line: r.endLine ?? null,
93
+ qualified_name: r.qualifiedName ?? null,
94
+ scope: r.scope ?? null,
95
+ visibility: (r.visibility ?? null) as ChildNodeRow['visibility'],
96
+ };
97
+ }
98
+
99
+ function toRelatedNodeRow(r: NativeRelatedNodeRow): RelatedNodeRow {
100
+ return {
101
+ id: r.id,
102
+ name: r.name,
103
+ kind: r.kind,
104
+ file: r.file,
105
+ line: r.line ?? 0,
106
+ end_line: r.endLine,
107
+ };
108
+ }
109
+
110
+ function toAdjacentEdgeRow(r: NativeAdjacentEdgeRow): AdjacentEdgeRow {
111
+ return {
112
+ name: r.name,
113
+ kind: r.kind,
114
+ file: r.file,
115
+ line: r.line ?? 0,
116
+ edge_kind: r.edgeKind as AdjacentEdgeRow['edge_kind'],
117
+ };
118
+ }
119
+
120
+ function toImportEdgeRow(r: NativeImportEdgeRow): ImportEdgeRow {
121
+ return { file: r.file, edge_kind: r.edgeKind as ImportEdgeRow['edge_kind'] };
122
+ }
123
+
124
+ function toIntraFileCallEdge(r: NativeIntraFileCallEdge): IntraFileCallEdge {
125
+ return { caller_name: r.callerName, callee_name: r.calleeName };
126
+ }
127
+
128
+ function toCallableNodeRow(r: NativeCallableNodeRow): CallableNodeRow {
129
+ return { id: r.id, name: r.name, kind: r.kind, file: r.file };
130
+ }
131
+
132
+ function toCallEdgeRow(r: NativeCallEdgeRow): CallEdgeRow {
133
+ return {
134
+ source_id: r.sourceId,
135
+ target_id: r.targetId,
136
+ confidence: r.confidence,
137
+ };
138
+ }
139
+
140
+ function toFileNodeRow(r: NativeFileNodeRow): FileNodeRow {
141
+ return { id: r.id, name: r.name, file: r.file };
142
+ }
143
+
144
+ function toImportGraphEdgeRow(r: NativeImportGraphEdgeRow): ImportGraphEdgeRow {
145
+ return { source_id: r.sourceId, target_id: r.targetId };
146
+ }
147
+
148
+ function toComplexityMetrics(r: NativeComplexityMetrics): ComplexityMetrics {
149
+ return {
150
+ cognitive: r.cognitive,
151
+ cyclomatic: r.cyclomatic,
152
+ max_nesting: r.maxNesting,
153
+ maintainability_index: r.maintainabilityIndex ?? null,
154
+ halstead_volume: r.halsteadVolume ?? null,
155
+ };
156
+ }
157
+
158
+ // ── NativeRepository ────────────────────────────────────────────────────
159
+
160
+ export class NativeRepository extends Repository {
161
+ #ndb: NativeDatabase;
162
+
163
+ constructor(ndb: NativeDatabase) {
164
+ super();
165
+ this.#ndb = ndb;
166
+ }
167
+
168
+ // ── Node lookups ──────────────────────────────────────────────────
169
+
170
+ findNodeById(id: number): NodeRow | undefined {
171
+ const r = this.#ndb.findNodeById(id);
172
+ return r ? toNodeRow(r) : undefined;
173
+ }
174
+
175
+ findNodesByFile(file: string): NodeRow[] {
176
+ return this.#ndb.findNodesByFile(file).map(toNodeRow);
177
+ }
178
+
179
+ findFileNodes(fileLike: string): NodeRow[] {
180
+ return this.#ndb.findFileNodes(fileLike).map(toNodeRow);
181
+ }
182
+
183
+ findNodesWithFanIn(namePattern: string, opts: QueryOpts = {}): NodeRowWithFanIn[] {
184
+ return this.#ndb
185
+ .findNodesWithFanIn(namePattern, opts.kinds ?? null, opts.file ?? null)
186
+ .map(toNodeRowWithFanIn);
187
+ }
188
+
189
+ countNodes(): number {
190
+ return this.#ndb.countNodes();
191
+ }
192
+
193
+ countEdges(): number {
194
+ return this.#ndb.countEdges();
195
+ }
196
+
197
+ countFiles(): number {
198
+ return this.#ndb.countFiles();
199
+ }
200
+
201
+ getNodeId(name: string, kind: string, file: string, line: number): number | undefined {
202
+ return this.#ndb.getNodeId(name, kind, file, line) ?? undefined;
203
+ }
204
+
205
+ getFunctionNodeId(name: string, file: string, line: number): number | undefined {
206
+ return this.#ndb.getFunctionNodeId(name, file, line) ?? undefined;
207
+ }
208
+
209
+ bulkNodeIdsByFile(file: string): NodeIdRow[] {
210
+ return this.#ndb.bulkNodeIdsByFile(file).map(toNodeIdRow);
211
+ }
212
+
213
+ findNodeChildren(parentId: number): ChildNodeRow[] {
214
+ return this.#ndb.findNodeChildren(parentId).map(toChildNodeRow);
215
+ }
216
+
217
+ findNodesByScope(scopeName: string, opts: QueryOpts = {}): NodeRow[] {
218
+ return this.#ndb
219
+ .findNodesByScope(scopeName, opts.kind ?? null, opts.file ?? null)
220
+ .map(toNodeRow);
221
+ }
222
+
223
+ findNodeByQualifiedName(qualifiedName: string, opts: { file?: string } = {}): NodeRow[] {
224
+ return this.#ndb.findNodeByQualifiedName(qualifiedName, opts.file ?? null).map(toNodeRow);
225
+ }
226
+
227
+ listFunctionNodes(opts: ListFunctionOpts = {}): NodeRow[] {
228
+ return this.#ndb
229
+ .listFunctionNodes(opts.file ?? null, opts.pattern ?? null, opts.noTests ?? null)
230
+ .map(toNodeRow);
231
+ }
232
+
233
+ iterateFunctionNodes(opts: ListFunctionOpts = {}): IterableIterator<NodeRow> {
234
+ const rows = this.#ndb
235
+ .iterateFunctionNodes(opts.file ?? null, opts.pattern ?? null, opts.noTests ?? null)
236
+ .map(toNodeRow);
237
+ return rows[Symbol.iterator]();
238
+ }
239
+
240
+ findNodesForTriage(opts: TriageQueryOpts = {}): TriageNodeRow[] {
241
+ try {
242
+ return this.#ndb
243
+ .findNodesForTriage(
244
+ opts.kind ?? null,
245
+ opts.role ?? null,
246
+ opts.file ?? null,
247
+ opts.noTests ?? null,
248
+ )
249
+ .map(toTriageNodeRow);
250
+ } catch (e: unknown) {
251
+ const msg = (e as Error).message;
252
+ if (msg.startsWith('Invalid kind:') || msg.startsWith('Invalid role:')) {
253
+ throw new ConfigError(msg);
254
+ }
255
+ throw e;
256
+ }
257
+ }
258
+
259
+ // ── Edge queries ──────────────────────────────────────────────────
260
+
261
+ findCallees(nodeId: number): RelatedNodeRow[] {
262
+ return this.#ndb.findCallees(nodeId).map(toRelatedNodeRow);
263
+ }
264
+
265
+ findCallers(nodeId: number): RelatedNodeRow[] {
266
+ return this.#ndb.findCallers(nodeId).map(toRelatedNodeRow);
267
+ }
268
+
269
+ findDistinctCallers(nodeId: number): RelatedNodeRow[] {
270
+ return this.#ndb.findDistinctCallers(nodeId).map(toRelatedNodeRow);
271
+ }
272
+
273
+ findAllOutgoingEdges(nodeId: number): AdjacentEdgeRow[] {
274
+ return this.#ndb.findAllOutgoingEdges(nodeId).map(toAdjacentEdgeRow);
275
+ }
276
+
277
+ findAllIncomingEdges(nodeId: number): AdjacentEdgeRow[] {
278
+ return this.#ndb.findAllIncomingEdges(nodeId).map(toAdjacentEdgeRow);
279
+ }
280
+
281
+ findCalleeNames(nodeId: number): string[] {
282
+ return this.#ndb.findCalleeNames(nodeId);
283
+ }
284
+
285
+ findCallerNames(nodeId: number): string[] {
286
+ return this.#ndb.findCallerNames(nodeId);
287
+ }
288
+
289
+ findImportTargets(nodeId: number): ImportEdgeRow[] {
290
+ return this.#ndb.findImportTargets(nodeId).map(toImportEdgeRow);
291
+ }
292
+
293
+ findImportSources(nodeId: number): ImportEdgeRow[] {
294
+ return this.#ndb.findImportSources(nodeId).map(toImportEdgeRow);
295
+ }
296
+
297
+ findImportDependents(nodeId: number): NodeRow[] {
298
+ return this.#ndb.findImportDependents(nodeId).map(toNodeRow);
299
+ }
300
+
301
+ findCrossFileCallTargets(file: string): Set<number> {
302
+ return new Set(this.#ndb.findCrossFileCallTargets(file));
303
+ }
304
+
305
+ countCrossFileCallers(nodeId: number, file: string): number {
306
+ return this.#ndb.countCrossFileCallers(nodeId, file);
307
+ }
308
+
309
+ getClassHierarchy(classNodeId: number): Set<number> {
310
+ return new Set(this.#ndb.getClassHierarchy(classNodeId));
311
+ }
312
+
313
+ findImplementors(nodeId: number): RelatedNodeRow[] {
314
+ return this.#ndb.findImplementors(nodeId).map(toRelatedNodeRow);
315
+ }
316
+
317
+ findInterfaces(nodeId: number): RelatedNodeRow[] {
318
+ return this.#ndb.findInterfaces(nodeId).map(toRelatedNodeRow);
319
+ }
320
+
321
+ findIntraFileCallEdges(file: string): IntraFileCallEdge[] {
322
+ return this.#ndb.findIntraFileCallEdges(file).map(toIntraFileCallEdge);
323
+ }
324
+
325
+ // ── Graph-read queries ────────────────────────────────────────────
326
+
327
+ getCallableNodes(): CallableNodeRow[] {
328
+ return this.#ndb.getCallableNodes().map(toCallableNodeRow);
329
+ }
330
+
331
+ getCallEdges(): CallEdgeRow[] {
332
+ return this.#ndb.getCallEdges().map(toCallEdgeRow);
333
+ }
334
+
335
+ getFileNodesAll(): FileNodeRow[] {
336
+ return this.#ndb.getFileNodesAll().map(toFileNodeRow);
337
+ }
338
+
339
+ getImportEdges(): ImportGraphEdgeRow[] {
340
+ return this.#ndb.getImportEdges().map(toImportGraphEdgeRow);
341
+ }
342
+
343
+ // ── Optional table checks ─────────────────────────────────────────
344
+
345
+ hasCfgTables(): boolean {
346
+ return this.#ndb.hasCfgTables();
347
+ }
348
+
349
+ hasEmbeddings(): boolean {
350
+ return this.#ndb.hasEmbeddings();
351
+ }
352
+
353
+ hasDataflowTable(): boolean {
354
+ return this.#ndb.hasDataflowTable();
355
+ }
356
+
357
+ getComplexityForNode(nodeId: number): ComplexityMetrics | undefined {
358
+ const r = this.#ndb.getComplexityForNode(nodeId);
359
+ return r ? toComplexityMetrics(r) : undefined;
360
+ }
361
+ }
@@ -4,6 +4,7 @@ import type {
4
4
  BetterSqlite3Database,
5
5
  ChildNodeRow,
6
6
  ListFunctionOpts,
7
+ NativeDatabase,
7
8
  NodeIdRow,
8
9
  NodeRow,
9
10
  NodeRowWithFanIn,
@@ -24,6 +25,7 @@ export function findNodesWithFanIn(
24
25
  db: BetterSqlite3Database,
25
26
  namePattern: string,
26
27
  opts: QueryOpts = {},
28
+ nativeDb?: NativeDatabase,
27
29
  ): NodeRowWithFanIn[] {
28
30
  const q = new NodeQuery()
29
31
  .select('n.*, COALESCE(fi.cnt, 0) AS fan_in')
@@ -37,7 +39,7 @@ export function findNodesWithFanIn(
37
39
  q.fileFilter(opts.file);
38
40
  }
39
41
 
40
- return q.all(db);
42
+ return q.all(db, nativeDb);
41
43
  }
42
44
 
43
45
  /**
@@ -46,6 +48,7 @@ export function findNodesWithFanIn(
46
48
  export function findNodesForTriage(
47
49
  db: BetterSqlite3Database,
48
50
  opts: TriageQueryOpts = {},
51
+ nativeDb?: NativeDatabase,
49
52
  ): TriageNodeRow[] {
50
53
  if (opts.kind && !(EVERY_SYMBOL_KIND as readonly string[]).includes(opts.kind)) {
51
54
  throw new ConfigError(
@@ -77,7 +80,7 @@ export function findNodesForTriage(
77
80
  .roleFilter(opts.role)
78
81
  .orderBy('n.file, n.line');
79
82
 
80
- return q.all(db);
83
+ return q.all(db, nativeDb);
81
84
  }
82
85
 
83
86
  /**
@@ -99,8 +102,9 @@ function _functionNodeQuery(opts: ListFunctionOpts = {}): InstanceType<typeof No
99
102
  export function listFunctionNodes(
100
103
  db: BetterSqlite3Database,
101
104
  opts: ListFunctionOpts = {},
105
+ nativeDb?: NativeDatabase,
102
106
  ): NodeRow[] {
103
- return _functionNodeQuery(opts).all(db);
107
+ return _functionNodeQuery(opts).all(db, nativeDb);
104
108
  }
105
109
 
106
110
  /**
@@ -12,6 +12,7 @@ import type {
12
12
  ExtractorOutput,
13
13
  FileToParse,
14
14
  MetadataUpdate,
15
+ NativeDatabase,
15
16
  NodeRow,
16
17
  ParseChange,
17
18
  PathAliases,
@@ -31,6 +32,7 @@ export class PipelineContext {
31
32
  incremental!: boolean;
32
33
  forceFullRebuild: boolean = false;
33
34
  schemaVersion!: number;
35
+ nativeDb?: NativeDatabase;
34
36
 
35
37
  // ── File collection (set by collectFiles stage) ────────────────────
36
38
  allFiles!: string[];
@@ -6,9 +6,10 @@
6
6
  */
7
7
  import path from 'node:path';
8
8
  import { performance } from 'node:perf_hooks';
9
- import { closeDb, getBuildMeta, initSchema, MIGRATIONS, openDb } from '../../../db/index.js';
9
+ import { closeDbPair, getBuildMeta, initSchema, MIGRATIONS, openDb } from '../../../db/index.js';
10
10
  import { detectWorkspaces, loadConfig } from '../../../infrastructure/config.js';
11
11
  import { info, warn } from '../../../infrastructure/logger.js';
12
+ import { loadNative } from '../../../infrastructure/native.js';
12
13
  import { CODEGRAPH_VERSION } from '../../../shared/version.js';
13
14
  import type { BuildGraphOpts, BuildResult } from '../../../types.js';
14
15
  import { getActiveEngine } from '../../parser.js';
@@ -33,6 +34,7 @@ function initializeEngine(ctx: PipelineContext): void {
33
34
  engine: ctx.opts.engine || 'auto',
34
35
  dataflow: ctx.opts.dataflow !== false,
35
36
  ast: ctx.opts.ast !== false,
37
+ nativeDb: ctx.nativeDb,
36
38
  };
37
39
  const { name: engineName, version: engineVersion } = getActiveEngine(ctx.engineOpts);
38
40
  ctx.engineName = engineName as 'native' | 'wasm';
@@ -46,19 +48,23 @@ function checkEngineSchemaMismatch(ctx: PipelineContext): void {
46
48
  ctx.forceFullRebuild = false;
47
49
  if (!ctx.incremental) return;
48
50
 
49
- const prevEngine = getBuildMeta(ctx.db, 'engine');
51
+ // Route metadata reads through NativeDatabase when available (Phase 6.13)
52
+ const meta = (key: string): string | null =>
53
+ ctx.nativeDb ? ctx.nativeDb.getBuildMeta(key) : getBuildMeta(ctx.db, key);
54
+
55
+ const prevEngine = meta('engine');
50
56
  if (prevEngine && prevEngine !== ctx.engineName) {
51
57
  info(`Engine changed (${prevEngine} → ${ctx.engineName}), promoting to full rebuild.`);
52
58
  ctx.forceFullRebuild = true;
53
59
  }
54
- const prevSchema = getBuildMeta(ctx.db, 'schema_version');
60
+ const prevSchema = meta('schema_version');
55
61
  if (prevSchema && Number(prevSchema) !== ctx.schemaVersion) {
56
62
  info(
57
63
  `Schema version changed (${prevSchema} → ${ctx.schemaVersion}), promoting to full rebuild.`,
58
64
  );
59
65
  ctx.forceFullRebuild = true;
60
66
  }
61
- const prevVersion = getBuildMeta(ctx.db, 'codegraph_version');
67
+ const prevVersion = meta('codegraph_version');
62
68
  if (prevVersion && prevVersion !== CODEGRAPH_VERSION) {
63
69
  info(
64
70
  `Codegraph version changed (${prevVersion} → ${CODEGRAPH_VERSION}), promoting to full rebuild.`,
@@ -91,7 +97,23 @@ function setupPipeline(ctx: PipelineContext): void {
91
97
  ctx.rootDir = path.resolve(ctx.rootDir);
92
98
  ctx.dbPath = path.join(ctx.rootDir, '.codegraph', 'graph.db');
93
99
  ctx.db = openDb(ctx.dbPath);
94
- initSchema(ctx.db);
100
+
101
+ // Use NativeDatabase for schema init when native engine is available (Phase 6.13).
102
+ // better-sqlite3 (ctx.db) is still always opened — needed for queries and stages
103
+ // that haven't been migrated to rusqlite yet.
104
+ const native = loadNative();
105
+ if (native?.NativeDatabase) {
106
+ try {
107
+ ctx.nativeDb = native.NativeDatabase.openReadWrite(ctx.dbPath);
108
+ ctx.nativeDb.initSchema();
109
+ } catch (err) {
110
+ warn(`NativeDatabase init failed, falling back to JS: ${(err as Error).message}`);
111
+ ctx.nativeDb = undefined;
112
+ initSchema(ctx.db);
113
+ }
114
+ } else {
115
+ initSchema(ctx.db);
116
+ }
95
117
 
96
118
  ctx.config = loadConfig(ctx.rootDir);
97
119
  ctx.incremental =
@@ -168,7 +190,9 @@ export async function buildGraph(
168
190
  setupPipeline(ctx);
169
191
  await runPipelineStages(ctx);
170
192
  } catch (err) {
171
- if (!ctx.earlyExit && ctx.db) closeDb(ctx.db);
193
+ if (!ctx.earlyExit && ctx.db) {
194
+ closeDbPair({ db: ctx.db, nativeDb: ctx.nativeDb });
195
+ }
172
196
  throw err;
173
197
  }
174
198