@optave/codegraph 3.9.0 → 3.9.2

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 (196) hide show
  1. package/README.md +12 -13
  2. package/dist/ast-analysis/engine.d.ts.map +1 -1
  3. package/dist/ast-analysis/engine.js +78 -48
  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 +15 -18
  7. package/dist/ast-analysis/visitors/ast-store-visitor.js.map +1 -1
  8. package/dist/cli/commands/batch.d.ts.map +1 -1
  9. package/dist/cli/commands/batch.js +5 -17
  10. package/dist/cli/commands/batch.js.map +1 -1
  11. package/dist/cli/commands/structure.d.ts.map +1 -1
  12. package/dist/cli/commands/structure.js +18 -1
  13. package/dist/cli/commands/structure.js.map +1 -1
  14. package/dist/db/connection.d.ts +3 -0
  15. package/dist/db/connection.d.ts.map +1 -1
  16. package/dist/db/connection.js +24 -6
  17. package/dist/db/connection.js.map +1 -1
  18. package/dist/db/index.d.ts +1 -1
  19. package/dist/db/index.d.ts.map +1 -1
  20. package/dist/db/index.js +1 -1
  21. package/dist/db/index.js.map +1 -1
  22. package/dist/db/repository/base.d.ts +35 -0
  23. package/dist/db/repository/base.d.ts.map +1 -1
  24. package/dist/db/repository/base.js +8 -0
  25. package/dist/db/repository/base.js.map +1 -1
  26. package/dist/db/repository/index.d.ts +1 -0
  27. package/dist/db/repository/index.d.ts.map +1 -1
  28. package/dist/db/repository/index.js.map +1 -1
  29. package/dist/db/repository/native-repository.d.ts +7 -1
  30. package/dist/db/repository/native-repository.d.ts.map +1 -1
  31. package/dist/db/repository/native-repository.js +46 -1
  32. package/dist/db/repository/native-repository.js.map +1 -1
  33. package/dist/domain/analysis/context.d.ts.map +1 -1
  34. package/dist/domain/analysis/context.js +5 -15
  35. package/dist/domain/analysis/context.js.map +1 -1
  36. package/dist/domain/analysis/dependencies.d.ts +6 -33
  37. package/dist/domain/analysis/dependencies.d.ts.map +1 -1
  38. package/dist/domain/analysis/dependencies.js +18 -16
  39. package/dist/domain/analysis/dependencies.js.map +1 -1
  40. package/dist/domain/analysis/fn-impact.js +2 -2
  41. package/dist/domain/analysis/fn-impact.js.map +1 -1
  42. package/dist/domain/analysis/implementations.d.ts.map +1 -1
  43. package/dist/domain/analysis/implementations.js +3 -13
  44. package/dist/domain/analysis/implementations.js.map +1 -1
  45. package/dist/domain/graph/builder/context.d.ts +4 -0
  46. package/dist/domain/graph/builder/context.d.ts.map +1 -1
  47. package/dist/domain/graph/builder/context.js +4 -0
  48. package/dist/domain/graph/builder/context.js.map +1 -1
  49. package/dist/domain/graph/builder/incremental.d.ts.map +1 -1
  50. package/dist/domain/graph/builder/incremental.js +18 -0
  51. package/dist/domain/graph/builder/incremental.js.map +1 -1
  52. package/dist/domain/graph/builder/native-db-proxy.d.ts +24 -0
  53. package/dist/domain/graph/builder/native-db-proxy.d.ts.map +1 -0
  54. package/dist/domain/graph/builder/native-db-proxy.js +87 -0
  55. package/dist/domain/graph/builder/native-db-proxy.js.map +1 -0
  56. package/dist/domain/graph/builder/pipeline.d.ts.map +1 -1
  57. package/dist/domain/graph/builder/pipeline.js +410 -349
  58. package/dist/domain/graph/builder/pipeline.js.map +1 -1
  59. package/dist/domain/graph/builder/stages/build-edges.d.ts.map +1 -1
  60. package/dist/domain/graph/builder/stages/build-edges.js +44 -4
  61. package/dist/domain/graph/builder/stages/build-edges.js.map +1 -1
  62. package/dist/domain/graph/builder/stages/build-structure.js +2 -2
  63. package/dist/domain/graph/builder/stages/build-structure.js.map +1 -1
  64. package/dist/domain/graph/builder/stages/detect-changes.d.ts.map +1 -1
  65. package/dist/domain/graph/builder/stages/detect-changes.js +6 -28
  66. package/dist/domain/graph/builder/stages/detect-changes.js.map +1 -1
  67. package/dist/domain/graph/builder/stages/finalize.js +1 -1
  68. package/dist/domain/graph/builder/stages/finalize.js.map +1 -1
  69. package/dist/domain/graph/builder/stages/insert-nodes.d.ts.map +1 -1
  70. package/dist/domain/graph/builder/stages/insert-nodes.js +16 -12
  71. package/dist/domain/graph/builder/stages/insert-nodes.js.map +1 -1
  72. package/dist/domain/graph/builder/stages/resolve-imports.d.ts.map +1 -1
  73. package/dist/domain/graph/builder/stages/resolve-imports.js +21 -26
  74. package/dist/domain/graph/builder/stages/resolve-imports.js.map +1 -1
  75. package/dist/domain/graph/watcher.d.ts.map +1 -1
  76. package/dist/domain/graph/watcher.js +99 -95
  77. package/dist/domain/graph/watcher.js.map +1 -1
  78. package/dist/domain/parser.d.ts.map +1 -1
  79. package/dist/domain/parser.js +7 -2
  80. package/dist/domain/parser.js.map +1 -1
  81. package/dist/domain/queries.d.ts +1 -1
  82. package/dist/domain/queries.d.ts.map +1 -1
  83. package/dist/domain/queries.js +1 -1
  84. package/dist/domain/queries.js.map +1 -1
  85. package/dist/extractors/go.js +53 -35
  86. package/dist/extractors/go.js.map +1 -1
  87. package/dist/extractors/javascript.js +66 -27
  88. package/dist/extractors/javascript.js.map +1 -1
  89. package/dist/features/audit.d.ts.map +1 -1
  90. package/dist/features/audit.js +3 -2
  91. package/dist/features/audit.js.map +1 -1
  92. package/dist/features/boundaries.d.ts.map +1 -1
  93. package/dist/features/boundaries.js +3 -5
  94. package/dist/features/boundaries.js.map +1 -1
  95. package/dist/features/branch-compare.d.ts.map +1 -1
  96. package/dist/features/branch-compare.js +2 -1
  97. package/dist/features/branch-compare.js.map +1 -1
  98. package/dist/features/complexity.d.ts.map +1 -1
  99. package/dist/features/complexity.js +78 -58
  100. package/dist/features/complexity.js.map +1 -1
  101. package/dist/features/dataflow.d.ts.map +1 -1
  102. package/dist/features/dataflow.js +109 -118
  103. package/dist/features/dataflow.js.map +1 -1
  104. package/dist/features/flow.d.ts.map +1 -1
  105. package/dist/features/flow.js +2 -1
  106. package/dist/features/flow.js.map +1 -1
  107. package/dist/features/manifesto.d.ts.map +1 -1
  108. package/dist/features/manifesto.js +15 -1
  109. package/dist/features/manifesto.js.map +1 -1
  110. package/dist/features/structure.d.ts.map +1 -1
  111. package/dist/features/structure.js +147 -97
  112. package/dist/features/structure.js.map +1 -1
  113. package/dist/graph/algorithms/louvain.d.ts.map +1 -1
  114. package/dist/graph/algorithms/louvain.js +4 -2
  115. package/dist/graph/algorithms/louvain.js.map +1 -1
  116. package/dist/graph/classifiers/roles.d.ts +2 -0
  117. package/dist/graph/classifiers/roles.d.ts.map +1 -1
  118. package/dist/graph/classifiers/roles.js +13 -5
  119. package/dist/graph/classifiers/roles.js.map +1 -1
  120. package/dist/infrastructure/config.d.ts +1 -0
  121. package/dist/infrastructure/config.d.ts.map +1 -1
  122. package/dist/infrastructure/config.js +1 -0
  123. package/dist/infrastructure/config.js.map +1 -1
  124. package/dist/presentation/batch.d.ts.map +1 -1
  125. package/dist/presentation/batch.js +1 -0
  126. package/dist/presentation/batch.js.map +1 -1
  127. package/dist/presentation/communities.d.ts.map +1 -1
  128. package/dist/presentation/communities.js +38 -34
  129. package/dist/presentation/communities.js.map +1 -1
  130. package/dist/presentation/manifesto.d.ts.map +1 -1
  131. package/dist/presentation/manifesto.js +31 -33
  132. package/dist/presentation/manifesto.js.map +1 -1
  133. package/dist/presentation/queries-cli/inspect.d.ts.map +1 -1
  134. package/dist/presentation/queries-cli/inspect.js +47 -46
  135. package/dist/presentation/queries-cli/inspect.js.map +1 -1
  136. package/dist/presentation/structure.d.ts +1 -1
  137. package/dist/presentation/structure.d.ts.map +1 -1
  138. package/dist/presentation/structure.js +1 -1
  139. package/dist/presentation/structure.js.map +1 -1
  140. package/dist/shared/file-utils.d.ts.map +1 -1
  141. package/dist/shared/file-utils.js +94 -72
  142. package/dist/shared/file-utils.js.map +1 -1
  143. package/dist/shared/normalize.d.ts +12 -0
  144. package/dist/shared/normalize.d.ts.map +1 -1
  145. package/dist/shared/normalize.js +4 -0
  146. package/dist/shared/normalize.js.map +1 -1
  147. package/dist/types.d.ts +82 -1
  148. package/dist/types.d.ts.map +1 -1
  149. package/package.json +7 -7
  150. package/src/ast-analysis/engine.ts +99 -55
  151. package/src/ast-analysis/visitors/ast-store-visitor.ts +19 -21
  152. package/src/cli/commands/batch.ts +5 -26
  153. package/src/cli/commands/structure.ts +21 -1
  154. package/src/db/connection.ts +26 -7
  155. package/src/db/index.ts +2 -0
  156. package/src/db/repository/base.ts +43 -0
  157. package/src/db/repository/index.ts +1 -0
  158. package/src/db/repository/native-repository.ts +67 -1
  159. package/src/domain/analysis/context.ts +5 -15
  160. package/src/domain/analysis/dependencies.ts +19 -16
  161. package/src/domain/analysis/fn-impact.ts +2 -2
  162. package/src/domain/analysis/implementations.ts +3 -13
  163. package/src/domain/graph/builder/context.ts +4 -0
  164. package/src/domain/graph/builder/incremental.ts +21 -0
  165. package/src/domain/graph/builder/native-db-proxy.ts +98 -0
  166. package/src/domain/graph/builder/pipeline.ts +514 -416
  167. package/src/domain/graph/builder/stages/build-edges.ts +45 -3
  168. package/src/domain/graph/builder/stages/build-structure.ts +2 -2
  169. package/src/domain/graph/builder/stages/detect-changes.ts +11 -33
  170. package/src/domain/graph/builder/stages/finalize.ts +1 -1
  171. package/src/domain/graph/builder/stages/insert-nodes.ts +17 -14
  172. package/src/domain/graph/builder/stages/resolve-imports.ts +22 -23
  173. package/src/domain/graph/watcher.ts +118 -98
  174. package/src/domain/parser.ts +8 -2
  175. package/src/domain/queries.ts +1 -1
  176. package/src/extractors/go.ts +57 -32
  177. package/src/extractors/javascript.ts +67 -27
  178. package/src/features/audit.ts +3 -2
  179. package/src/features/boundaries.ts +3 -5
  180. package/src/features/branch-compare.ts +2 -3
  181. package/src/features/complexity.ts +94 -58
  182. package/src/features/dataflow.ts +153 -132
  183. package/src/features/flow.ts +2 -1
  184. package/src/features/manifesto.ts +15 -1
  185. package/src/features/structure.ts +167 -95
  186. package/src/graph/algorithms/louvain.ts +5 -2
  187. package/src/graph/classifiers/roles.ts +14 -5
  188. package/src/infrastructure/config.ts +1 -0
  189. package/src/presentation/batch.ts +1 -0
  190. package/src/presentation/communities.ts +44 -39
  191. package/src/presentation/manifesto.ts +35 -38
  192. package/src/presentation/queries-cli/inspect.ts +48 -46
  193. package/src/presentation/structure.ts +2 -2
  194. package/src/shared/file-utils.ts +116 -77
  195. package/src/shared/normalize.ts +10 -0
  196. package/src/types.ts +86 -0
@@ -1,26 +1,11 @@
1
1
  import fs from 'node:fs';
2
2
  import { collectFile } from '../../db/query-builder.js';
3
3
  import { EVERY_SYMBOL_KIND } from '../../domain/queries.js';
4
- import { BATCH_COMMANDS, multiBatchData, splitTargets } from '../../features/batch.js';
5
- import { batch } from '../../presentation/batch.js';
4
+ import { BATCH_COMMANDS, splitTargets } from '../../features/batch.js';
5
+ import { batchQuery } from '../../presentation/batch.js';
6
6
  import { ConfigError, toErrorMessage } from '../../shared/errors.js';
7
7
  import type { CommandDefinition } from '../types.js';
8
8
 
9
- interface MultiBatchItem {
10
- command: string;
11
- target: string;
12
- opts?: Record<string, unknown>;
13
- }
14
-
15
- function isMultiBatch(targets: unknown[]): targets is MultiBatchItem[] {
16
- return (
17
- targets.length > 0 &&
18
- typeof targets[0] === 'object' &&
19
- targets[0] !== null &&
20
- 'command' in targets[0]
21
- );
22
- }
23
-
24
9
  export const command: CommandDefinition = {
25
10
  name: 'batch <command> [targets...]',
26
11
  description: `Run a query against multiple targets in one call. Output is always JSON.\nValid commands: ${Object.keys(BATCH_COMMANDS).join(', ')}`,
@@ -69,18 +54,12 @@ export const command: CommandDefinition = {
69
54
  );
70
55
  }
71
56
 
72
- const batchOpts = {
57
+ batchQuery(targets as Array<string | { command: string; target: string }>, opts.db, {
58
+ command,
73
59
  depth: opts.depth ? parseInt(opts.depth as string, 10) : undefined,
74
60
  file: opts.file,
75
61
  kind: opts.kind,
76
62
  noTests: ctx.resolveNoTests(opts),
77
- };
78
-
79
- if (isMultiBatch(targets)) {
80
- const data = multiBatchData(targets as MultiBatchItem[], opts.db, batchOpts);
81
- console.log(JSON.stringify(data, null, 2));
82
- } else {
83
- batch(command!, targets as string[], opts.db, batchOpts);
84
- }
63
+ });
85
64
  },
86
65
  };
@@ -15,9 +15,29 @@ export const command: CommandDefinition = {
15
15
  ['--limit <number>', 'Max results to return'],
16
16
  ['--offset <number>', 'Skip N results (default: 0)'],
17
17
  ['--ndjson', 'Newline-delimited JSON output'],
18
+ ['--modules', 'Show module boundaries (directories with high cohesion)'],
19
+ ['--threshold <number>', 'Cohesion threshold for --modules (default: 0.3)'],
18
20
  ],
19
21
  async execute([dir], opts, ctx) {
20
- const { structureData, formatStructure } = await import('../../presentation/structure.js');
22
+ const { structureData, formatStructure, moduleBoundariesData, formatModuleBoundaries } =
23
+ await import('../../presentation/structure.js');
24
+
25
+ if (opts.modules) {
26
+ const parsed = opts.threshold ? parseFloat(opts.threshold as string) : undefined;
27
+ if (parsed !== undefined && Number.isNaN(parsed)) {
28
+ console.error('Error: --threshold must be a number');
29
+ process.exitCode = 1;
30
+ return;
31
+ }
32
+ const data = moduleBoundariesData(opts.db, {
33
+ threshold: parsed,
34
+ });
35
+ if (!ctx.outputResult(data, 'modules', opts)) {
36
+ console.log(formatModuleBoundaries(data));
37
+ }
38
+ return;
39
+ }
40
+
21
41
  const qOpts = ctx.resolveQueryOpts(opts);
22
42
  const data = structureData(opts.db, {
23
43
  directory: dir,
@@ -109,7 +109,7 @@ function isProcessAlive(pid: number): boolean {
109
109
  }
110
110
  }
111
111
 
112
- function acquireAdvisoryLock(dbPath: string): void {
112
+ export function acquireAdvisoryLock(dbPath: string): void {
113
113
  const lockPath = `${dbPath}.lock`;
114
114
  try {
115
115
  if (fs.existsSync(lockPath)) {
@@ -129,7 +129,7 @@ function acquireAdvisoryLock(dbPath: string): void {
129
129
  }
130
130
  }
131
131
 
132
- function releaseAdvisoryLock(lockPath: string): void {
132
+ export function releaseAdvisoryLock(lockPath: string): void {
133
133
  try {
134
134
  const content = fs.readFileSync(lockPath, 'utf-8').trim();
135
135
  if (Number(content) === process.pid) {
@@ -311,6 +311,7 @@ export function openReadonlyOrFail(customPath?: string): BetterSqlite3Database {
311
311
  }
312
312
  const Database = getDatabase();
313
313
  const db = new Database(dbPath, { readonly: true }) as unknown as BetterSqlite3Database;
314
+ db.pragma('busy_timeout = 5000');
314
315
 
315
316
  warnOnVersionMismatch(() => {
316
317
  const row = db
@@ -359,7 +360,7 @@ function openRepoNative(customDbPath?: string): { repo: Repository; close(): voi
359
360
  */
360
361
  export function openRepo(
361
362
  customDbPath?: string,
362
- opts: { repo?: Repository } = {},
363
+ opts: { repo?: Repository; engine?: 'native' | 'wasm' | 'auto' } = {},
363
364
  ): { repo: Repository; close(): void } {
364
365
  if (opts.repo != null) {
365
366
  if (!(opts.repo instanceof Repository)) {
@@ -370,15 +371,25 @@ export function openRepo(
370
371
  return { repo: opts.repo, close() {} };
371
372
  }
372
373
 
374
+ // Respect explicit engine selection: opts.engine > CODEGRAPH_ENGINE env > auto.
375
+ // This ensures --engine wasm and benchmark workers bypass the native path.
376
+ const engine = opts.engine || process.env.CODEGRAPH_ENGINE || 'auto';
377
+
373
378
  // Try native rusqlite path first (Phase 6.14)
374
- if (isNativeAvailable()) {
379
+ if (engine !== 'wasm' && isNativeAvailable()) {
375
380
  try {
376
381
  return openRepoNative(customDbPath);
377
382
  } catch (e) {
378
383
  // Re-throw user-visible errors (e.g. DB not found) — only silently
379
384
  // fall back for native-engine failures (e.g. incompatible native binary).
380
385
  if (e instanceof DbError) throw e;
381
- debug(`openRepo: native path failed, falling back to better-sqlite3: ${toErrorMessage(e)}`);
386
+ // Re-throw locking/busy errors falling back to better-sqlite3 would
387
+ // hit the same contention (and potentially hang without busy_timeout).
388
+ const msg = toErrorMessage(e);
389
+ if (/\b(busy|locked|SQLITE_BUSY|SQLITE_LOCKED)\b/i.test(msg)) {
390
+ throw new DbError(`Database is busy (another process may be writing): ${msg}`, {});
391
+ }
392
+ debug(`openRepo: native path failed, falling back to better-sqlite3: ${msg}`);
382
393
  }
383
394
  }
384
395
 
@@ -405,14 +416,22 @@ export function openReadonlyWithNative(customPath?: string): {
405
416
  } {
406
417
  const db = openReadonlyOrFail(customPath);
407
418
 
419
+ // Respect explicit engine selection, consistent with openRepo().
420
+ const engine = process.env.CODEGRAPH_ENGINE || 'auto';
421
+
408
422
  let nativeDb: NativeDatabase | undefined;
409
- if (isNativeAvailable()) {
423
+ if (engine !== 'wasm' && isNativeAvailable()) {
410
424
  try {
411
425
  const dbPath = findDbPath(customPath);
412
426
  const native = getNative();
413
427
  nativeDb = native.NativeDatabase.openReadonly(dbPath);
414
428
  } catch (e) {
415
- debug(`openReadonlyWithNative: native path failed: ${toErrorMessage(e)}`);
429
+ const msg = toErrorMessage(e);
430
+ if (/\b(busy|locked|SQLITE_BUSY|SQLITE_LOCKED)\b/i.test(msg)) {
431
+ debug(`openReadonlyWithNative: native path busy, skipping native DB: ${msg}`);
432
+ } else {
433
+ debug(`openReadonlyWithNative: native path failed: ${msg}`);
434
+ }
416
435
  }
417
436
  }
418
437
 
package/src/db/index.ts CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  export type { LockedDatabase, LockedDatabasePair } from './connection.js';
4
4
  export {
5
+ acquireAdvisoryLock,
5
6
  closeDb,
6
7
  closeDbDeferred,
7
8
  closeDbPair,
@@ -13,6 +14,7 @@ export {
13
14
  openReadonlyOrFail,
14
15
  openReadonlyWithNative,
15
16
  openRepo,
17
+ releaseAdvisoryLock,
16
18
  } from './connection.js';
17
19
  export { getBuildMeta, initSchema, MIGRATIONS, setBuildMeta } from './migrations.js';
18
20
  export {
@@ -223,4 +223,47 @@ export class Repository implements IRepository {
223
223
  hasCoChangesTable(): boolean {
224
224
  return false;
225
225
  }
226
+
227
+ // ── Composite queries ──────────────────────────────────────────────
228
+ /**
229
+ * Complete fnDeps query in a single call. Returns null when not natively
230
+ * supported — callers should fall back to the JS-orchestrated path.
231
+ */
232
+ fnDeps(
233
+ _name: string,
234
+ _opts?: { depth?: number; noTests?: boolean; file?: string; kind?: string },
235
+ ): FnDepsResult | null {
236
+ return null;
237
+ }
238
+ }
239
+
240
+ // ── Composite query result types ────────────────────────────────────────
241
+
242
+ export interface FnDepsNode {
243
+ name: string;
244
+ kind: string;
245
+ file: string;
246
+ line: number | null;
247
+ }
248
+
249
+ export interface FnDepsCallerNode extends FnDepsNode {
250
+ viaHierarchy?: string;
251
+ }
252
+
253
+ export interface FnDepsEntry {
254
+ name: string;
255
+ kind: string;
256
+ file: string;
257
+ line: number | null;
258
+ endLine: number | null;
259
+ role: string | null;
260
+ fileHash: string | null;
261
+ callees: FnDepsNode[];
262
+ callers: FnDepsCallerNode[];
263
+ transitiveCallers: Record<number, FnDepsNode[]>;
264
+ }
265
+
266
+ export interface FnDepsResult {
267
+ name: string;
268
+ results: FnDepsEntry[];
226
269
  }
@@ -1,5 +1,6 @@
1
1
  // Barrel re-export for repository/ modules.
2
2
 
3
+ export type { FnDepsCallerNode, FnDepsEntry, FnDepsNode, FnDepsResult } from './base.js';
3
4
  export { Repository } from './base.js';
4
5
  export { purgeFileData, purgeFilesData } from './build-stmts.js';
5
6
  export { cachedStmt } from './cached-stmt.js';
@@ -45,7 +45,13 @@ import type {
45
45
  TriageNodeRow,
46
46
  TriageQueryOpts,
47
47
  } from '../../types.js';
48
- import { Repository } from './base.js';
48
+ import {
49
+ type FnDepsCallerNode,
50
+ type FnDepsEntry,
51
+ type FnDepsNode,
52
+ type FnDepsResult,
53
+ Repository,
54
+ } from './base.js';
49
55
 
50
56
  // ── Row converters (napi camelCase → Repository snake_case) ─────────────
51
57
 
@@ -461,4 +467,64 @@ export class NativeRepository extends Repository {
461
467
  }
462
468
  return false;
463
469
  }
470
+
471
+ // ── Composite queries ──────────────────────────────────────────────
472
+ fnDeps(
473
+ name: string,
474
+ opts?: { depth?: number; noTests?: boolean; file?: string; kind?: string },
475
+ ): FnDepsResult | null {
476
+ if (typeof this.#ndb.fnDeps !== 'function') return null;
477
+ const raw = this.#ndb.fnDeps(
478
+ name,
479
+ opts?.depth ?? undefined,
480
+ opts?.noTests ?? undefined,
481
+ opts?.file ?? undefined,
482
+ opts?.kind ?? undefined,
483
+ );
484
+ // Convert from native format (transitive_callers as array of groups)
485
+ // to JS format (transitiveCallers as Record<number, Array>)
486
+ return {
487
+ name: raw.name,
488
+ results: raw.results.map((entry: any): FnDepsEntry => {
489
+ const transitiveCallers: Record<number, FnDepsNode[]> = {};
490
+ for (const group of entry.transitiveCallers ?? []) {
491
+ transitiveCallers[group.depth] = (group.callers ?? []).map(
492
+ (c: any): FnDepsNode => ({
493
+ name: c.name,
494
+ kind: c.kind,
495
+ file: c.file,
496
+ line: c.line ?? null,
497
+ }),
498
+ );
499
+ }
500
+ return {
501
+ name: entry.name,
502
+ kind: entry.kind,
503
+ file: entry.file,
504
+ line: entry.line ?? null,
505
+ endLine: entry.endLine ?? entry.end_line ?? null,
506
+ role: entry.role ?? null,
507
+ fileHash: entry.fileHash ?? entry.file_hash ?? null,
508
+ callees: (entry.callees ?? []).map(
509
+ (c: any): FnDepsNode => ({
510
+ name: c.name,
511
+ kind: c.kind,
512
+ file: c.file,
513
+ line: c.line ?? null,
514
+ }),
515
+ ),
516
+ callers: (entry.callers ?? []).map(
517
+ (c: any): FnDepsCallerNode => ({
518
+ name: c.name,
519
+ kind: c.kind,
520
+ file: c.file,
521
+ line: c.line ?? null,
522
+ viaHierarchy: c.viaHierarchy ?? c.via_hierarchy ?? undefined,
523
+ }),
524
+ ),
525
+ transitiveCallers,
526
+ };
527
+ }),
528
+ };
529
+ }
464
530
  }
@@ -27,7 +27,7 @@ import {
27
27
  readSourceRange,
28
28
  } from '../../shared/file-utils.js';
29
29
  import { resolveMethodViaHierarchy } from '../../shared/hierarchy.js';
30
- import { normalizeSymbol } from '../../shared/normalize.js';
30
+ import { normalizeSymbol, toSymbolRef } from '../../shared/normalize.js';
31
31
  import { paginateResult } from '../../shared/paginate.js';
32
32
  import type {
33
33
  BetterSqlite3Database,
@@ -177,7 +177,7 @@ function buildImplementationInfo(db: BetterSqlite3Database, node: NodeRow, noTes
177
177
  let impls = findImplementors(db, node.id) as RelatedNodeRow[];
178
178
  if (noTests) impls = impls.filter((n) => !isTestFile(n.file));
179
179
  return {
180
- implementors: impls.map((n) => ({ name: n.name, kind: n.kind, file: n.file, line: n.line })),
180
+ implementors: impls.map(toSymbolRef),
181
181
  };
182
182
  }
183
183
  // For classes/structs: show what they implement
@@ -186,7 +186,7 @@ function buildImplementationInfo(db: BetterSqlite3Database, node: NodeRow, noTes
186
186
  if (noTests) ifaces = ifaces.filter((n) => !isTestFile(n.file));
187
187
  if (ifaces.length > 0) {
188
188
  return {
189
- implements: ifaces.map((n) => ({ name: n.name, kind: n.kind, file: n.file, line: n.line })),
189
+ implements: ifaces.map(toSymbolRef),
190
190
  };
191
191
  }
192
192
  }
@@ -359,21 +359,11 @@ function explainFunctionImpl(
359
359
  const summary = fileLines ? extractSummary(fileLines, node.line, displayOpts) : null;
360
360
  const signature = fileLines ? extractSignature(fileLines, node.line, displayOpts) : null;
361
361
 
362
- const callees = (findCallees(db, node.id) as RelatedNodeRow[]).map((c) => ({
363
- name: c.name,
364
- kind: c.kind,
365
- file: c.file,
366
- line: c.line,
367
- }));
362
+ const callees = (findCallees(db, node.id) as RelatedNodeRow[]).map(toSymbolRef);
368
363
 
369
364
  const allCallerRows = findCallers(db, node.id) as RelatedNodeRow[];
370
365
 
371
- let callers = allCallerRows.map((c) => ({
372
- name: c.name,
373
- kind: c.kind,
374
- file: c.file,
375
- line: c.line,
376
- }));
366
+ let callers = allCallerRows.map(toSymbolRef);
377
367
  if (noTests) callers = callers.filter((c) => !isTestFile(c.file));
378
368
 
379
369
  const seenFiles = new Set<string>();
@@ -2,7 +2,7 @@ import { findFileNodes, type Repository } from '../../db/index.js';
2
2
  import { cachedStmt } from '../../db/repository/cached-stmt.js';
3
3
  import { isTestFile } from '../../infrastructure/test-filter.js';
4
4
  import { resolveMethodViaHierarchy } from '../../shared/hierarchy.js';
5
- import { normalizeSymbol } from '../../shared/normalize.js';
5
+ import { normalizeSymbol, toSymbolRef } from '../../shared/normalize.js';
6
6
  import { paginateResult } from '../../shared/paginate.js';
7
7
  import type {
8
8
  BetterSqlite3Database,
@@ -143,12 +143,8 @@ function buildNodeDepsResult(
143
143
 
144
144
  return {
145
145
  ...normalizeSymbol(node, repo, hc),
146
- callees: filteredCallees.map((c) => ({
147
- name: c.name,
148
- kind: c.kind,
149
- file: c.file,
150
- line: c.line,
151
- })),
146
+ callees: filteredCallees.map(toSymbolRef),
147
+ // Not using toSymbolRef — callers include the extra viaHierarchy field
152
148
  callers: callers.map((c) => ({
153
149
  name: c.name,
154
150
  kind: c.kind,
@@ -173,6 +169,19 @@ export function fnDepsData(
173
169
  } = {},
174
170
  ) {
175
171
  return withRepo(customDbPath, (repo) => {
172
+ // Try native composite path — single NAPI call for the entire query.
173
+ const nativeResult = repo.fnDeps(name, {
174
+ depth: opts.depth,
175
+ noTests: opts.noTests,
176
+ file: opts.file,
177
+ kind: opts.kind,
178
+ });
179
+ if (nativeResult) {
180
+ const base = { name: nativeResult.name, results: nativeResult.results };
181
+ return paginateResult(base, 'results', { limit: opts.limit, offset: opts.offset });
182
+ }
183
+
184
+ // Fallback: JS-orchestrated path (used when native engine is unavailable)
176
185
  const depth = opts.depth || 3;
177
186
  const noTests = opts.noTests || false;
178
187
  const hc = new Map();
@@ -232,20 +241,14 @@ function resolveEndpoints(
232
241
  to,
233
242
  found: false,
234
243
  error: `No symbol matching "${to}"`,
235
- fromCandidates: fromNodes
236
- .slice(0, 5)
237
- .map((n) => ({ name: n.name, kind: n.kind, file: n.file, line: n.line })),
244
+ fromCandidates: fromNodes.slice(0, 5).map(toSymbolRef),
238
245
  toCandidates: [],
239
246
  },
240
247
  };
241
248
  }
242
249
 
243
- const fromCandidates = fromNodes
244
- .slice(0, 5)
245
- .map((n) => ({ name: n.name, kind: n.kind, file: n.file, line: n.line }));
246
- const toCandidates = toNodes
247
- .slice(0, 5)
248
- .map((n) => ({ name: n.name, kind: n.kind, file: n.file, line: n.line }));
250
+ const fromCandidates = fromNodes.slice(0, 5).map(toSymbolRef);
251
+ const toCandidates = toNodes.slice(0, 5).map(toSymbolRef);
249
252
 
250
253
  return {
251
254
  sourceNode: fromNodes[0],
@@ -1,6 +1,6 @@
1
1
  import { Repository, SqliteRepository } from '../../db/index.js';
2
2
  import { isTestFile } from '../../infrastructure/test-filter.js';
3
- import { normalizeSymbol } from '../../shared/normalize.js';
3
+ import { normalizeSymbol, toSymbolRef } from '../../shared/normalize.js';
4
4
  import { paginateResult } from '../../shared/paginate.js';
5
5
  import type { BetterSqlite3Database, NodeRow, RelatedNodeRow } from '../../types.js';
6
6
  import { resolveAnalysisOpts, withRepo } from './query-helpers.js';
@@ -125,7 +125,7 @@ export function bfsTransitiveCallers(
125
125
  visited.add(c.id);
126
126
  nextFrontier.push(c.id);
127
127
  if (!levels[d]) levels[d] = [];
128
- levels[d]!.push({ name: c.name, kind: c.kind, file: c.file, line: c.line });
128
+ levels[d]!.push(toSymbolRef(c));
129
129
  if (onVisit) onVisit(c, fid, d);
130
130
  }
131
131
  if (resolveImplementors && INTERFACE_LIKE_KINDS.has(c.kind)) {
@@ -1,6 +1,6 @@
1
1
  import { isTestFile } from '../../infrastructure/test-filter.js';
2
2
  import { CORE_SYMBOL_KINDS } from '../../shared/kinds.js';
3
- import { normalizeSymbol } from '../../shared/normalize.js';
3
+ import { normalizeSymbol, toSymbolRef } from '../../shared/normalize.js';
4
4
  import { paginateResult } from '../../shared/paginate.js';
5
5
  import type { RelatedNodeRow } from '../../types.js';
6
6
  import { withRepo } from './query-helpers.js';
@@ -34,12 +34,7 @@ export function implementationsData(
34
34
 
35
35
  return {
36
36
  ...normalizeSymbol(node, repo, hc),
37
- implementors: implementors.map((impl) => ({
38
- name: impl.name,
39
- kind: impl.kind,
40
- file: impl.file,
41
- line: impl.line,
42
- })),
37
+ implementors: implementors.map(toSymbolRef),
43
38
  };
44
39
  });
45
40
 
@@ -76,12 +71,7 @@ export function interfacesData(
76
71
 
77
72
  return {
78
73
  ...normalizeSymbol(node, repo, hc),
79
- interfaces: interfaces.map((iface) => ({
80
- name: iface.name,
81
- kind: iface.kind,
82
- file: iface.file,
83
- line: iface.line,
84
- })),
74
+ interfaces: interfaces.map(toSymbolRef),
85
75
  };
86
76
  });
87
77
 
@@ -33,6 +33,10 @@ export class PipelineContext {
33
33
  forceFullRebuild: boolean = false;
34
34
  schemaVersion!: number;
35
35
  nativeDb?: NativeDatabase;
36
+ /** Whether native engine is available (deferred — DB opened only when needed). */
37
+ nativeAvailable: boolean = false;
38
+ /** True when ctx.db is a NativeDbProxy — single rusqlite connection for the entire pipeline. */
39
+ nativeFirstProxy: boolean = false;
36
40
 
37
41
  // ── File collection (set by collectFiles stage) ────────────────────
38
42
  allFiles!: string[];
@@ -366,6 +366,27 @@ function buildImportEdges(
366
366
  stmts.insertEdge.run(fileNodeId, targetRow.id, edgeKind, 1.0, 0);
367
367
  edgesAdded++;
368
368
 
369
+ // Type-only imports: create symbol-level edges so the target symbols
370
+ // get fan-in credit and aren't falsely classified as dead code.
371
+ if (imp.typeOnly) {
372
+ for (const name of imp.names) {
373
+ const cleanName = name.replace(/^\*\s+as\s+/, '');
374
+ let targetFile = resolvedPath;
375
+ if (db && isBarrelFile(db, resolvedPath)) {
376
+ const actual = resolveBarrelTarget(db, resolvedPath, cleanName);
377
+ if (actual) targetFile = actual;
378
+ }
379
+ const candidates = stmts.findNodeInFile.all(cleanName, targetFile) as Array<{
380
+ id: number;
381
+ file: string;
382
+ }>;
383
+ if (candidates.length > 0) {
384
+ stmts.insertEdge.run(fileNodeId, candidates[0]!.id, 'imports-type', 1.0, 0);
385
+ edgesAdded++;
386
+ }
387
+ }
388
+ }
389
+
369
390
  // Barrel resolution: create edges through re-export chains
370
391
  if (!imp.reexport && db) {
371
392
  edgesAdded += resolveBarrelImportEdges(db, stmts, fileNodeId, resolvedPath, imp);
@@ -0,0 +1,98 @@
1
+ /**
2
+ * NativeDbProxy — wraps a NativeDatabase (rusqlite via napi-rs) to satisfy the
3
+ * BetterSqlite3Database interface. When the native addon is available, the
4
+ * build pipeline uses this proxy as `ctx.db` so that every stage operates on a
5
+ * single rusqlite connection — no dual-connection WAL corruption, no
6
+ * open/close/reopen dance.
7
+ *
8
+ * When native is unavailable, the pipeline falls back to real better-sqlite3.
9
+ */
10
+
11
+ import type { BetterSqlite3Database, NativeDatabase, SqliteStatement } from '../../../types.js';
12
+
13
+ /** Sanitize params for napi-rs: better-sqlite3 treats `undefined` as NULL,
14
+ * but serde_json cannot represent `undefined`. Replace with `null`. */
15
+ function sanitize(params: unknown[]): Array<string | number | null> {
16
+ return params.map((p) => (p === undefined ? null : p)) as Array<string | number | null>;
17
+ }
18
+
19
+ export class NativeDbProxy implements BetterSqlite3Database {
20
+ readonly #ndb: NativeDatabase;
21
+ /** Advisory lock path — set by the pipeline so closeDb() can release it. */
22
+ __lockPath?: string;
23
+
24
+ constructor(nativeDb: NativeDatabase) {
25
+ this.#ndb = nativeDb;
26
+ }
27
+
28
+ prepare<TRow = unknown>(sql: string): SqliteStatement<TRow> {
29
+ const ndb = this.#ndb;
30
+ const stmt: SqliteStatement<TRow> = {
31
+ all(...params: unknown[]): TRow[] {
32
+ return ndb.queryAll(sql, sanitize(params)) as TRow[];
33
+ },
34
+ get(...params: unknown[]): TRow | undefined {
35
+ return (ndb.queryGet(sql, sanitize(params)) ?? undefined) as TRow | undefined;
36
+ },
37
+ run(...params: unknown[]): { changes: number; lastInsertRowid: number | bigint } {
38
+ ndb.queryAll(sql, sanitize(params));
39
+ // Retrieve last_insert_rowid via SQLite scalar function so callers
40
+ // that depend on it (e.g. CFG block edge mapping) get correct values.
41
+ const row = ndb.queryGet('SELECT last_insert_rowid() AS rid', []) as { rid: number } | null;
42
+ return { changes: 0, lastInsertRowid: row?.rid ?? 0 };
43
+ },
44
+ iterate(): IterableIterator<TRow> {
45
+ throw new Error('iterate() is not supported via NativeDbProxy');
46
+ },
47
+ raw(): SqliteStatement<TRow> {
48
+ return stmt; // no-op — .raw() is not used in the build pipeline
49
+ },
50
+ };
51
+ return stmt;
52
+ }
53
+
54
+ exec(sql: string): this {
55
+ this.#ndb.exec(sql);
56
+ return this;
57
+ }
58
+
59
+ pragma(sql: string): unknown {
60
+ return this.#ndb.pragma(sql);
61
+ }
62
+
63
+ close(): void {
64
+ // No-op: the pipeline manages the NativeDatabase lifecycle directly.
65
+ // closeDbPair() calls nativeDb.close() separately.
66
+ }
67
+
68
+ get open(): boolean {
69
+ return this.#ndb.isOpen;
70
+ }
71
+
72
+ get name(): string {
73
+ return this.#ndb.dbPath;
74
+ }
75
+
76
+ transaction<F extends (...args: any[]) => any>(
77
+ fn: F,
78
+ ): (...args: F extends (...a: infer A) => unknown ? A : never) => ReturnType<F> {
79
+ const ndb = this.#ndb;
80
+ return ((...args: unknown[]) => {
81
+ // NOTE: nested transactions (savepoints) are not supported — ensure callers
82
+ // do not invoke a transaction() wrapper from within an existing transaction.
83
+ ndb.exec('BEGIN');
84
+ try {
85
+ const result = fn(...args);
86
+ ndb.exec('COMMIT');
87
+ return result;
88
+ } catch (e) {
89
+ try {
90
+ ndb.exec('ROLLBACK');
91
+ } catch {
92
+ // Ignore rollback errors — the original error is more important
93
+ }
94
+ throw e;
95
+ }
96
+ }) as any;
97
+ }
98
+ }