@optave/codegraph 3.9.0 → 3.9.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 (104) hide show
  1. package/README.md +7 -6
  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/db/connection.d.ts +1 -0
  9. package/dist/db/connection.d.ts.map +1 -1
  10. package/dist/db/connection.js +22 -4
  11. package/dist/db/connection.js.map +1 -1
  12. package/dist/db/repository/base.d.ts +35 -0
  13. package/dist/db/repository/base.d.ts.map +1 -1
  14. package/dist/db/repository/base.js +8 -0
  15. package/dist/db/repository/base.js.map +1 -1
  16. package/dist/db/repository/index.d.ts +1 -0
  17. package/dist/db/repository/index.d.ts.map +1 -1
  18. package/dist/db/repository/index.js.map +1 -1
  19. package/dist/db/repository/native-repository.d.ts +7 -1
  20. package/dist/db/repository/native-repository.d.ts.map +1 -1
  21. package/dist/db/repository/native-repository.js +46 -1
  22. package/dist/db/repository/native-repository.js.map +1 -1
  23. package/dist/domain/analysis/dependencies.d.ts +1 -28
  24. package/dist/domain/analysis/dependencies.d.ts.map +1 -1
  25. package/dist/domain/analysis/dependencies.js +12 -0
  26. package/dist/domain/analysis/dependencies.js.map +1 -1
  27. package/dist/domain/graph/builder/incremental.d.ts.map +1 -1
  28. package/dist/domain/graph/builder/incremental.js +18 -0
  29. package/dist/domain/graph/builder/incremental.js.map +1 -1
  30. package/dist/domain/graph/builder/pipeline.d.ts.map +1 -1
  31. package/dist/domain/graph/builder/pipeline.js +293 -296
  32. package/dist/domain/graph/builder/pipeline.js.map +1 -1
  33. package/dist/domain/graph/builder/stages/build-edges.d.ts.map +1 -1
  34. package/dist/domain/graph/builder/stages/build-edges.js +29 -2
  35. package/dist/domain/graph/builder/stages/build-edges.js.map +1 -1
  36. package/dist/domain/graph/builder/stages/resolve-imports.d.ts.map +1 -1
  37. package/dist/domain/graph/builder/stages/resolve-imports.js +19 -23
  38. package/dist/domain/graph/builder/stages/resolve-imports.js.map +1 -1
  39. package/dist/domain/graph/watcher.d.ts.map +1 -1
  40. package/dist/domain/graph/watcher.js +99 -95
  41. package/dist/domain/graph/watcher.js.map +1 -1
  42. package/dist/domain/parser.d.ts.map +1 -1
  43. package/dist/domain/parser.js +2 -0
  44. package/dist/domain/parser.js.map +1 -1
  45. package/dist/extractors/go.js +53 -35
  46. package/dist/extractors/go.js.map +1 -1
  47. package/dist/extractors/javascript.js +66 -27
  48. package/dist/extractors/javascript.js.map +1 -1
  49. package/dist/features/complexity.d.ts.map +1 -1
  50. package/dist/features/complexity.js +78 -58
  51. package/dist/features/complexity.js.map +1 -1
  52. package/dist/features/dataflow.d.ts.map +1 -1
  53. package/dist/features/dataflow.js +109 -118
  54. package/dist/features/dataflow.js.map +1 -1
  55. package/dist/features/structure.d.ts.map +1 -1
  56. package/dist/features/structure.js +147 -97
  57. package/dist/features/structure.js.map +1 -1
  58. package/dist/graph/algorithms/louvain.d.ts.map +1 -1
  59. package/dist/graph/algorithms/louvain.js +4 -2
  60. package/dist/graph/algorithms/louvain.js.map +1 -1
  61. package/dist/graph/classifiers/roles.d.ts +2 -0
  62. package/dist/graph/classifiers/roles.d.ts.map +1 -1
  63. package/dist/graph/classifiers/roles.js +13 -5
  64. package/dist/graph/classifiers/roles.js.map +1 -1
  65. package/dist/presentation/communities.d.ts.map +1 -1
  66. package/dist/presentation/communities.js +38 -34
  67. package/dist/presentation/communities.js.map +1 -1
  68. package/dist/presentation/manifesto.d.ts.map +1 -1
  69. package/dist/presentation/manifesto.js +31 -33
  70. package/dist/presentation/manifesto.js.map +1 -1
  71. package/dist/presentation/queries-cli/inspect.d.ts.map +1 -1
  72. package/dist/presentation/queries-cli/inspect.js +47 -46
  73. package/dist/presentation/queries-cli/inspect.js.map +1 -1
  74. package/dist/shared/file-utils.d.ts.map +1 -1
  75. package/dist/shared/file-utils.js +94 -72
  76. package/dist/shared/file-utils.js.map +1 -1
  77. package/dist/types.d.ts +81 -1
  78. package/dist/types.d.ts.map +1 -1
  79. package/package.json +7 -7
  80. package/src/ast-analysis/engine.ts +99 -55
  81. package/src/ast-analysis/visitors/ast-store-visitor.ts +19 -21
  82. package/src/db/connection.ts +24 -5
  83. package/src/db/repository/base.ts +43 -0
  84. package/src/db/repository/index.ts +1 -0
  85. package/src/db/repository/native-repository.ts +67 -1
  86. package/src/domain/analysis/dependencies.ts +13 -0
  87. package/src/domain/graph/builder/incremental.ts +21 -0
  88. package/src/domain/graph/builder/pipeline.ts +392 -362
  89. package/src/domain/graph/builder/stages/build-edges.ts +30 -1
  90. package/src/domain/graph/builder/stages/resolve-imports.ts +20 -20
  91. package/src/domain/graph/watcher.ts +118 -98
  92. package/src/domain/parser.ts +2 -0
  93. package/src/extractors/go.ts +57 -32
  94. package/src/extractors/javascript.ts +67 -27
  95. package/src/features/complexity.ts +94 -58
  96. package/src/features/dataflow.ts +153 -132
  97. package/src/features/structure.ts +167 -95
  98. package/src/graph/algorithms/louvain.ts +5 -2
  99. package/src/graph/classifiers/roles.ts +14 -5
  100. package/src/presentation/communities.ts +44 -39
  101. package/src/presentation/manifesto.ts +35 -38
  102. package/src/presentation/queries-cli/inspect.ts +48 -46
  103. package/src/shared/file-utils.ts +116 -77
  104. package/src/types.ts +85 -0
@@ -215,25 +215,37 @@ function runNativeAnalysis(
215
215
  }
216
216
  }
217
217
 
218
+ /** Index native results by line number and match to a definition by name. */
219
+ function indexNativeByLine<T extends { line: number; name: string }>(
220
+ results: T[],
221
+ ): Map<number, T[]> {
222
+ const byLine = new Map<number, T[]>();
223
+ for (const r of results) {
224
+ if (!byLine.has(r.line)) byLine.set(r.line, []);
225
+ byLine.get(r.line)!.push(r);
226
+ }
227
+ return byLine;
228
+ }
229
+
230
+ function matchNativeResult<T extends { name: string }>(
231
+ candidates: T[] | undefined,
232
+ defName: string,
233
+ ): T | undefined {
234
+ if (!candidates) return undefined;
235
+ if (candidates.length === 1) return candidates[0];
236
+ return candidates.find((r) => r.name === defName) ?? candidates[0];
237
+ }
238
+
218
239
  /** Store native complexity results on definitions, matched by line number. */
219
240
  function storeNativeComplexityResults(
220
241
  results: NativeFunctionComplexityResult[],
221
242
  defs: Definition[],
222
243
  ): void {
223
- const byLine = new Map<number, NativeFunctionComplexityResult[]>();
224
- for (const r of results) {
225
- if (!byLine.has(r.line)) byLine.set(r.line, []);
226
- byLine.get(r.line)!.push(r);
227
- }
244
+ const byLine = indexNativeByLine(results);
228
245
 
229
246
  for (const def of defs) {
230
247
  if ((def.kind === 'function' || def.kind === 'method') && def.line && !def.complexity) {
231
- const candidates = byLine.get(def.line);
232
- if (!candidates) continue;
233
- const match =
234
- candidates.length === 1
235
- ? candidates[0]
236
- : (candidates.find((r) => r.name === def.name) ?? candidates[0]);
248
+ const match = matchNativeResult(byLine.get(def.line), def.name);
237
249
  if (!match) continue;
238
250
  const { complexity: c } = match;
239
251
  def.complexity = {
@@ -284,11 +296,7 @@ function overrideCyclomaticFromCfg(def: Definition, cfgCyclomatic: number): void
284
296
 
285
297
  /** Store native CFG results on definitions, matched by line number. */
286
298
  function storeNativeCfgResults(results: NativeFunctionCfgResult[], defs: Definition[]): void {
287
- const byLine = new Map<number, NativeFunctionCfgResult[]>();
288
- for (const r of results) {
289
- if (!byLine.has(r.line)) byLine.set(r.line, []);
290
- byLine.get(r.line)!.push(r);
291
- }
299
+ const byLine = indexNativeByLine(results);
292
300
 
293
301
  for (const def of defs) {
294
302
  if (
@@ -297,12 +305,7 @@ function storeNativeCfgResults(results: NativeFunctionCfgResult[], defs: Definit
297
305
  def.cfg !== null &&
298
306
  !def.cfg?.blocks?.length
299
307
  ) {
300
- const candidates = byLine.get(def.line);
301
- if (!candidates) continue;
302
- const match =
303
- candidates.length === 1
304
- ? candidates[0]
305
- : (candidates.find((r) => r.name === def.name) ?? candidates[0]);
308
+ const match = matchNativeResult(byLine.get(def.line), def.name);
306
309
  if (!match) continue;
307
310
  def.cfg = match.cfg;
308
311
 
@@ -353,42 +356,61 @@ function reconcileCfgCyclomatic(fileSymbols: Map<string, ExtractorOutput>): void
353
356
 
354
357
  // ─── WASM pre-parse ─────────────────────────────────────────────────────
355
358
 
359
+ /** Check whether a single file needs a WASM tree for any enabled analysis pass. */
360
+ function fileNeedsWasmTree(
361
+ relPath: string,
362
+ symbols: ExtractorOutput,
363
+ flags: { doAst: boolean; doComplexity: boolean; doCfg: boolean; doDataflow: boolean },
364
+ ): boolean {
365
+ if (symbols._tree) return false;
366
+ const ext = path.extname(relPath).toLowerCase();
367
+ const defs = symbols.definitions || [];
368
+ const lid = symbols._langId || '';
369
+
370
+ if (
371
+ flags.doAst &&
372
+ !Array.isArray(symbols.astNodes) &&
373
+ (WALK_EXTENSIONS.has(ext) || AST_TYPE_MAPS.has(lid))
374
+ )
375
+ return true;
376
+ if (
377
+ flags.doComplexity &&
378
+ (COMPLEXITY_EXTENSIONS.has(ext) || COMPLEXITY_RULES.has(lid)) &&
379
+ defs.some((d) => hasFuncBody(d) && !d.complexity)
380
+ )
381
+ return true;
382
+ if (
383
+ flags.doCfg &&
384
+ (CFG_EXTENSIONS.has(ext) || CFG_RULES.has(lid)) &&
385
+ defs.some((d) => hasFuncBody(d) && d.cfg !== null && !Array.isArray(d.cfg?.blocks))
386
+ )
387
+ return true;
388
+ if (
389
+ flags.doDataflow &&
390
+ !symbols.dataflow &&
391
+ (DATAFLOW_EXTENSIONS.has(ext) || DATAFLOW_RULES.has(lid))
392
+ )
393
+ return true;
394
+ return false;
395
+ }
396
+
356
397
  async function ensureWasmTreesIfNeeded(
357
398
  fileSymbols: Map<string, ExtractorOutput>,
358
399
  opts: AnalysisOpts,
359
400
  rootDir: string,
360
401
  ): Promise<void> {
361
- const doAst = opts.ast !== false;
362
- const doComplexity = opts.complexity !== false;
363
- const doCfg = opts.cfg !== false;
364
- const doDataflow = opts.dataflow !== false;
402
+ const flags = {
403
+ doAst: opts.ast !== false,
404
+ doComplexity: opts.complexity !== false,
405
+ doCfg: opts.cfg !== false,
406
+ doDataflow: opts.dataflow !== false,
407
+ };
365
408
 
366
- if (!doAst && !doComplexity && !doCfg && !doDataflow) return;
409
+ if (!flags.doAst && !flags.doComplexity && !flags.doCfg && !flags.doDataflow) return;
367
410
 
368
411
  let needsWasmTrees = false;
369
412
  for (const [relPath, symbols] of fileSymbols) {
370
- if (symbols._tree) continue;
371
- const ext = path.extname(relPath).toLowerCase();
372
- const defs = symbols.definitions || [];
373
-
374
- // AST: need tree when native didn't provide non-call astNodes
375
- const lid = symbols._langId || '';
376
- const needsAst =
377
- doAst &&
378
- !Array.isArray(symbols.astNodes) &&
379
- (WALK_EXTENSIONS.has(ext) || AST_TYPE_MAPS.has(lid));
380
- const needsComplexity =
381
- doComplexity &&
382
- (COMPLEXITY_EXTENSIONS.has(ext) || COMPLEXITY_RULES.has(lid)) &&
383
- defs.some((d) => hasFuncBody(d) && !d.complexity);
384
- const needsCfg =
385
- doCfg &&
386
- (CFG_EXTENSIONS.has(ext) || CFG_RULES.has(lid)) &&
387
- defs.some((d) => hasFuncBody(d) && d.cfg !== null && !Array.isArray(d.cfg?.blocks));
388
- const needsDataflow =
389
- doDataflow && !symbols.dataflow && (DATAFLOW_EXTENSIONS.has(ext) || DATAFLOW_RULES.has(lid));
390
-
391
- if (needsAst || needsComplexity || needsCfg || needsDataflow) {
413
+ if (fileNeedsWasmTree(relPath, symbols, flags)) {
392
414
  needsWasmTrees = true;
393
415
  break;
394
416
  }
@@ -607,7 +629,7 @@ async function delegateToBuildFunctions(
607
629
  } catch (err: unknown) {
608
630
  debug(`buildAstNodes failed: ${toErrorMessage(err)}`);
609
631
  }
610
- timing.astMs = performance.now() - t0;
632
+ timing.astMs += performance.now() - t0;
611
633
  }
612
634
 
613
635
  if (opts.complexity !== false) {
@@ -618,7 +640,7 @@ async function delegateToBuildFunctions(
618
640
  } catch (err: unknown) {
619
641
  debug(`buildComplexityMetrics failed: ${toErrorMessage(err)}`);
620
642
  }
621
- timing.complexityMs = performance.now() - t0;
643
+ timing.complexityMs += performance.now() - t0;
622
644
  }
623
645
 
624
646
  if (opts.cfg !== false) {
@@ -629,7 +651,7 @@ async function delegateToBuildFunctions(
629
651
  } catch (err: unknown) {
630
652
  debug(`buildCFGData failed: ${toErrorMessage(err)}`);
631
653
  }
632
- timing.cfgMs = performance.now() - t0;
654
+ timing.cfgMs += performance.now() - t0;
633
655
  }
634
656
 
635
657
  if (opts.dataflow !== false) {
@@ -640,7 +662,7 @@ async function delegateToBuildFunctions(
640
662
  } catch (err: unknown) {
641
663
  debug(`buildDataflowEdges failed: ${toErrorMessage(err)}`);
642
664
  }
643
- timing.dataflowMs = performance.now() - t0;
665
+ timing.dataflowMs += performance.now() - t0;
644
666
  }
645
667
  }
646
668
 
@@ -668,7 +690,7 @@ export async function runAnalyses(
668
690
  // This fills in complexity/CFG/dataflow for files that the native parse pipeline
669
691
  // missed, avoiding the need to parse with WASM + run JS visitors.
670
692
  const native = loadNative();
671
- if (native?.analyzeComplexity ?? native?.buildCfgAnalysis ?? native?.extractDataflowAnalysis) {
693
+ if (native?.analyzeComplexity || native?.buildCfgAnalysis || native?.extractDataflowAnalysis) {
672
694
  const t0native = performance.now();
673
695
  runNativeAnalysis(native, fileSymbols, rootDir, opts, extToLang);
674
696
  debug(`native standalone analysis: ${(performance.now() - t0native).toFixed(1)}ms`);
@@ -677,7 +699,10 @@ export async function runAnalyses(
677
699
  // WASM pre-parse for files that still need it (AST store, or native gaps)
678
700
  await ensureWasmTreesIfNeeded(fileSymbols, opts, rootDir);
679
701
 
680
- // Unified pre-walk: run all applicable visitors in a single DFS per file
702
+ // Unified pre-walk: run all applicable visitors in a single DFS per file.
703
+ // Time each file's walk and distribute equally among active visitors
704
+ // so that phase timers (astMs, complexityMs, etc.) reflect real work — not
705
+ // just the DB-write tail in delegateToBuildFunctions.
681
706
  const t0walk = performance.now();
682
707
 
683
708
  for (const [relPath, symbols] of fileSymbols) {
@@ -692,7 +717,22 @@ export async function runAnalyses(
692
717
 
693
718
  if (visitors.length === 0) continue;
694
719
 
720
+ const walkStart = performance.now();
695
721
  const results = walkWithVisitors(symbols._tree.rootNode, visitors, langId, walkerOpts);
722
+ const walkMs = performance.now() - walkStart;
723
+
724
+ // Distribute walk time equally among active visitors
725
+ const activeCount = [astVisitor, complexityVisitor, cfgVisitor, dataflowVisitor].filter(
726
+ Boolean,
727
+ ).length;
728
+ if (activeCount > 0) {
729
+ const share = walkMs / activeCount;
730
+ if (astVisitor) timing.astMs += share;
731
+ if (complexityVisitor) timing.complexityMs += share;
732
+ if (cfgVisitor) timing.cfgMs += share;
733
+ if (dataflowVisitor) timing.dataflowMs += share;
734
+ }
735
+
696
736
  const defs = symbols.definitions || [];
697
737
 
698
738
  if (astVisitor) {
@@ -705,6 +745,10 @@ export async function runAnalyses(
705
745
  if (dataflowVisitor) symbols.dataflow = results.dataflow as DataflowResult;
706
746
  }
707
747
 
748
+ // Total wall-clock time for the unified walk loop, including per-file
749
+ // setupVisitors overhead. Walk time is already distributed into per-phase
750
+ // timers above, so this field overlaps with (astMs + complexityMs + ...).
751
+ // It is kept as a diagnostic cross-check, not an additive bucket.
708
752
  timing._unifiedWalkMs = performance.now() - t0walk;
709
753
 
710
754
  // Reconcile: apply CFG-derived cyclomatic override for any definitions that have
@@ -102,27 +102,25 @@ export function createAstStoreVisitor(
102
102
  return nodeIdMap.get(`${parentDef.name}|${parentDef.kind}|${parentDef.line}`) || null;
103
103
  }
104
104
 
105
- function resolveNameAndText(
106
- node: TreeSitterNode,
107
- kind: string,
108
- ): { name: string | null | undefined; text: string | null; skip?: boolean } {
109
- switch (kind) {
110
- case 'new':
111
- return { name: extractNewName(node), text: truncate(node.text) };
112
- case 'throw':
113
- return { name: extractThrowName(node), text: extractExpressionText(node) };
114
- case 'await':
115
- return { name: extractAwaitName(node), text: extractExpressionText(node) };
116
- case 'string': {
117
- const content = node.text?.replace(/^['"`]|['"`]$/g, '') || '';
118
- if (content.length < 2) return { name: null, text: null, skip: true };
119
- return { name: truncate(content, 100), text: truncate(node.text) };
120
- }
121
- case 'regex':
122
- return { name: node.text || '?', text: truncate(node.text) };
123
- default:
124
- return { name: undefined, text: null };
125
- }
105
+ type NameTextResult = { name: string | null | undefined; text: string | null; skip?: boolean };
106
+ type KindHandler = (node: TreeSitterNode) => NameTextResult;
107
+
108
+ const kindHandlers: Record<string, KindHandler> = {
109
+ new: (node) => ({ name: extractNewName(node), text: truncate(node.text) }),
110
+ throw: (node) => ({ name: extractThrowName(node), text: extractExpressionText(node) }),
111
+ await: (node) => ({ name: extractAwaitName(node), text: extractExpressionText(node) }),
112
+ string: (node) => {
113
+ const content = node.text?.replace(/^['"`]|['"`]$/g, '') || '';
114
+ if (content.length < 2) return { name: null, text: null, skip: true };
115
+ return { name: truncate(content, 100), text: truncate(node.text) };
116
+ },
117
+ regex: (node) => ({ name: node.text || '?', text: truncate(node.text) }),
118
+ };
119
+ const defaultResult: NameTextResult = { name: undefined, text: null };
120
+
121
+ function resolveNameAndText(node: TreeSitterNode, kind: string): NameTextResult {
122
+ const handler = kindHandlers[kind];
123
+ return handler ? handler(node) : defaultResult;
126
124
  }
127
125
 
128
126
  function collectNode(node: TreeSitterNode, kind: string): void {
@@ -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
 
@@ -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
  }
@@ -173,6 +173,19 @@ export function fnDepsData(
173
173
  } = {},
174
174
  ) {
175
175
  return withRepo(customDbPath, (repo) => {
176
+ // Try native composite path — single NAPI call for the entire query.
177
+ const nativeResult = repo.fnDeps(name, {
178
+ depth: opts.depth,
179
+ noTests: opts.noTests,
180
+ file: opts.file,
181
+ kind: opts.kind,
182
+ });
183
+ if (nativeResult) {
184
+ const base = { name: nativeResult.name, results: nativeResult.results };
185
+ return paginateResult(base, 'results', { limit: opts.limit, offset: opts.offset });
186
+ }
187
+
188
+ // Fallback: JS-orchestrated path (used when native engine is unavailable)
176
189
  const depth = opts.depth || 3;
177
190
  const noTests = opts.noTests || false;
178
191
  const hc = new Map();
@@ -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);