@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
@@ -251,6 +251,391 @@ function refreshJsDb(ctx: PipelineContext): void {
251
251
  ctx.db = openDb(ctx.dbPath);
252
252
  }
253
253
 
254
+ // ── Native orchestrator types ──────────────────────────────────────────
255
+
256
+ interface NativeOrchestratorResult {
257
+ phases: Record<string, number>;
258
+ earlyExit?: boolean;
259
+ nodeCount?: number;
260
+ edgeCount?: number;
261
+ fileCount?: number;
262
+ changedFiles?: string[];
263
+ changedCount?: number;
264
+ removedCount?: number;
265
+ isFullBuild?: boolean;
266
+ /** Full changed files including reverse-dep files — used by JS structure fallback. */
267
+ structureScope?: string[];
268
+ /** Whether the Rust pipeline handled the structure phase (small-incremental fast path). */
269
+ structureHandled?: boolean;
270
+ }
271
+
272
+ // ── Native orchestrator helpers ───────────────────────────────────────
273
+
274
+ /** Determine whether the native orchestrator should be skipped. Returns a reason string, or null if it should run. */
275
+ function shouldSkipNativeOrchestrator(ctx: PipelineContext): string | null {
276
+ if (process.env.CODEGRAPH_FORCE_JS_PIPELINE === '1') return 'CODEGRAPH_FORCE_JS_PIPELINE=1';
277
+ if (ctx.forceFullRebuild) return 'forceFullRebuild';
278
+ const orchestratorBuggy = !!ctx.engineVersion && semverCompare(ctx.engineVersion, '3.10.0') < 0;
279
+ if (orchestratorBuggy) return `buggy addon ${ctx.engineVersion}`;
280
+ if (ctx.engineName !== 'native') return `engine=${ctx.engineName}`;
281
+ return null;
282
+ }
283
+
284
+ /** Checkpoint WAL through rusqlite, close nativeDb, and reopen better-sqlite3.
285
+ * Returns false if the DB reopen fails (caller should return partial result). */
286
+ function handoffWalAfterNativeBuild(ctx: PipelineContext): boolean {
287
+ closeNativeDb(ctx, 'post-native-build');
288
+ try {
289
+ ctx.db.close();
290
+ } catch (e) {
291
+ debug(`handoffWal JS db close failed: ${toErrorMessage(e)}`);
292
+ }
293
+ try {
294
+ ctx.db = openDb(ctx.dbPath);
295
+ return true;
296
+ } catch (reopenErr) {
297
+ warn(`Failed to reopen DB after native build: ${(reopenErr as Error).message}`);
298
+ return false;
299
+ }
300
+ }
301
+
302
+ /**
303
+ * Reconstruct fileSymbols from the DB after a native orchestrator build.
304
+ * When `scopeFiles` is provided, only loads those files (for analysis-only).
305
+ * When omitted, loads all files (needed for structure rebuilds).
306
+ */
307
+ function reconstructFileSymbolsFromDb(
308
+ ctx: PipelineContext,
309
+ scopeFiles?: string[],
310
+ ): Map<string, ExtractorOutput> {
311
+ let query =
312
+ 'SELECT file, name, kind, line, end_line as endLine FROM nodes WHERE file IS NOT NULL';
313
+ const params: string[] = [];
314
+ if (scopeFiles && scopeFiles.length > 0) {
315
+ const placeholders = scopeFiles.map(() => '?').join(',');
316
+ query += ` AND file IN (${placeholders})`;
317
+ params.push(...scopeFiles);
318
+ }
319
+ query += ' ORDER BY file, line';
320
+
321
+ const rows = ctx.db.prepare(query).all(...params) as {
322
+ file: string;
323
+ name: string;
324
+ kind: string;
325
+ line: number;
326
+ endLine: number | null;
327
+ }[];
328
+
329
+ const fileSymbols = new Map<string, ExtractorOutput>();
330
+ for (const row of rows) {
331
+ let entry = fileSymbols.get(row.file);
332
+ if (!entry) {
333
+ entry = {
334
+ definitions: [],
335
+ calls: [],
336
+ imports: [],
337
+ classes: [],
338
+ exports: [],
339
+ typeMap: new Map(),
340
+ };
341
+ fileSymbols.set(row.file, entry);
342
+ }
343
+ entry.definitions.push({
344
+ name: row.name,
345
+ kind: row.kind as Definition['kind'],
346
+ line: row.line,
347
+ endLine: row.endLine ?? undefined,
348
+ });
349
+ }
350
+
351
+ // Populate import/export counts from DB edges so buildStructure
352
+ // computes correct import_count/export_count in node_metrics.
353
+ // The extractor arrays aren't persisted to the DB, so we derive
354
+ // counts from edge data instead (#804).
355
+ const importCountRows = ctx.db
356
+ .prepare(
357
+ `SELECT n.file, COUNT(*) AS cnt
358
+ FROM edges e JOIN nodes n ON e.source_id = n.id
359
+ WHERE e.kind IN ('imports', 'imports-type', 'dynamic-imports')
360
+ AND n.file IS NOT NULL
361
+ GROUP BY n.file`,
362
+ )
363
+ .all() as { file: string; cnt: number }[];
364
+ for (const row of importCountRows) {
365
+ const entry = fileSymbols.get(row.file);
366
+ if (entry) entry.imports = new Array(row.cnt) as ExtractorOutput['imports'];
367
+ }
368
+
369
+ const exportCountRows = ctx.db
370
+ .prepare(
371
+ `SELECT n_tgt.file, COUNT(DISTINCT n_tgt.id) AS cnt
372
+ FROM edges e
373
+ JOIN nodes n_tgt ON e.target_id = n_tgt.id
374
+ JOIN nodes n_src ON e.source_id = n_src.id
375
+ WHERE e.kind IN ('imports', 'imports-type', 'reexports')
376
+ AND n_tgt.file IS NOT NULL
377
+ AND n_src.file != n_tgt.file
378
+ GROUP BY n_tgt.file`,
379
+ )
380
+ .all() as { file: string; cnt: number }[];
381
+ for (const row of exportCountRows) {
382
+ const entry = fileSymbols.get(row.file);
383
+ if (entry) entry.exports = new Array(row.cnt) as ExtractorOutput['exports'];
384
+ }
385
+
386
+ return fileSymbols;
387
+ }
388
+
389
+ /**
390
+ * Run JS buildStructure() after native orchestrator to fill directory nodes + contains edges.
391
+ * For full builds, passes changedFiles=null (full rebuild).
392
+ * For incremental builds, passes the changed file list to scope the update.
393
+ */
394
+ async function runPostNativeStructure(
395
+ ctx: PipelineContext,
396
+ allFileSymbols: Map<string, ExtractorOutput>,
397
+ isFullBuild: boolean,
398
+ changedFiles: string[] | undefined,
399
+ ): Promise<number> {
400
+ const structureStart = performance.now();
401
+ try {
402
+ const directories = new Set<string>();
403
+ for (const relPath of allFileSymbols.keys()) {
404
+ const parts = relPath.split('/');
405
+ for (let i = 1; i < parts.length; i++) {
406
+ directories.add(parts.slice(0, i).join('/'));
407
+ }
408
+ }
409
+
410
+ const lineCountMap = new Map<string, number>();
411
+ const cachedLineCounts = ctx.db
412
+ .prepare(
413
+ `SELECT n.name AS file, m.line_count
414
+ FROM node_metrics m JOIN nodes n ON m.node_id = n.id
415
+ WHERE n.kind = 'file'`,
416
+ )
417
+ .all() as Array<{ file: string; line_count: number }>;
418
+ for (const row of cachedLineCounts) {
419
+ lineCountMap.set(row.file, row.line_count);
420
+ }
421
+
422
+ // Full builds need null (rebuild everything). Incremental builds pass the
423
+ // changed file list so buildStructure only updates those files' metrics
424
+ // and contains edges — matching the JS pipeline's medium-incremental path.
425
+ const changedFilePaths = isFullBuild || !changedFiles?.length ? null : changedFiles;
426
+ const { buildStructure: buildStructureFn } = (await import(
427
+ '../../../features/structure.js'
428
+ )) as {
429
+ buildStructure: (
430
+ db: typeof ctx.db,
431
+ fileSymbols: Map<string, ExtractorOutput>,
432
+ rootDir: string,
433
+ lineCountMap: Map<string, number>,
434
+ directories: Set<string>,
435
+ changedFiles: string[] | null,
436
+ ) => void;
437
+ };
438
+ buildStructureFn(
439
+ ctx.db,
440
+ allFileSymbols,
441
+ ctx.rootDir,
442
+ lineCountMap,
443
+ directories,
444
+ changedFilePaths,
445
+ );
446
+ debug(
447
+ `Structure phase completed after native orchestrator${changedFilePaths ? ` (${changedFilePaths.length} files)` : ' (full)'}`,
448
+ );
449
+ } catch (err) {
450
+ warn(`Structure phase failed after native build: ${toErrorMessage(err)}`);
451
+ }
452
+ return performance.now() - structureStart;
453
+ }
454
+
455
+ /** Run AST/complexity/CFG/dataflow analysis after native orchestrator. */
456
+ async function runPostNativeAnalysis(
457
+ ctx: PipelineContext,
458
+ allFileSymbols: Map<string, ExtractorOutput>,
459
+ changedFiles: string[] | undefined,
460
+ ): Promise<{ astMs: number; complexityMs: number; cfgMs: number; dataflowMs: number }> {
461
+ const timing = { astMs: 0, complexityMs: 0, cfgMs: 0, dataflowMs: 0 };
462
+
463
+ // Scope analysis fileSymbols to changed files only
464
+ let analysisFileSymbols: Map<string, ExtractorOutput>;
465
+ if (changedFiles && changedFiles.length > 0) {
466
+ analysisFileSymbols = new Map();
467
+ for (const f of changedFiles) {
468
+ const entry = allFileSymbols.get(f);
469
+ if (entry) analysisFileSymbols.set(f, entry);
470
+ }
471
+ } else {
472
+ analysisFileSymbols = allFileSymbols;
473
+ }
474
+
475
+ // Reopen nativeDb for analysis features (suspend/resume WAL pattern).
476
+ const native = loadNative();
477
+ if (native?.NativeDatabase) {
478
+ try {
479
+ ctx.nativeDb = native.NativeDatabase.openReadWrite(ctx.dbPath);
480
+ if (ctx.engineOpts) ctx.engineOpts.nativeDb = ctx.nativeDb;
481
+ } catch {
482
+ ctx.nativeDb = undefined;
483
+ if (ctx.engineOpts) ctx.engineOpts.nativeDb = undefined;
484
+ }
485
+ }
486
+
487
+ try {
488
+ const { runAnalyses: runAnalysesFn } = await import('../../../ast-analysis/engine.js');
489
+ const result = await runAnalysesFn(
490
+ ctx.db,
491
+ analysisFileSymbols,
492
+ ctx.rootDir,
493
+ ctx.opts,
494
+ ctx.engineOpts,
495
+ );
496
+ timing.astMs = result.astMs ?? 0;
497
+ timing.complexityMs = result.complexityMs ?? 0;
498
+ timing.cfgMs = result.cfgMs ?? 0;
499
+ timing.dataflowMs = result.dataflowMs ?? 0;
500
+ } catch (err) {
501
+ warn(`Analysis phases failed after native build: ${toErrorMessage(err)}`);
502
+ }
503
+
504
+ // Close nativeDb after analyses
505
+ if (ctx.nativeDb) {
506
+ try {
507
+ ctx.nativeDb.exec('PRAGMA wal_checkpoint(TRUNCATE)');
508
+ } catch {
509
+ /* ignore checkpoint errors */
510
+ }
511
+ try {
512
+ ctx.nativeDb.close();
513
+ } catch {
514
+ /* ignore close errors */
515
+ }
516
+ ctx.nativeDb = undefined;
517
+ if (ctx.engineOpts) ctx.engineOpts.nativeDb = undefined;
518
+ }
519
+
520
+ return timing;
521
+ }
522
+
523
+ /** Format timing result from native orchestrator phases + JS post-processing. */
524
+ function formatNativeTimingResult(
525
+ p: Record<string, number>,
526
+ structurePatchMs: number,
527
+ analysisTiming: { astMs: number; complexityMs: number; cfgMs: number; dataflowMs: number },
528
+ ): BuildResult {
529
+ return {
530
+ phases: {
531
+ setupMs: +((p.setupMs ?? 0) + (p.collectMs ?? 0) + (p.detectMs ?? 0)).toFixed(1),
532
+ parseMs: +(p.parseMs ?? 0).toFixed(1),
533
+ insertMs: +(p.insertMs ?? 0).toFixed(1),
534
+ resolveMs: +(p.resolveMs ?? 0).toFixed(1),
535
+ edgesMs: +(p.edgesMs ?? 0).toFixed(1),
536
+ structureMs: +((p.structureMs ?? 0) + structurePatchMs).toFixed(1),
537
+ rolesMs: +(p.rolesMs ?? 0).toFixed(1),
538
+ astMs: +(analysisTiming.astMs ?? 0).toFixed(1),
539
+ complexityMs: +(analysisTiming.complexityMs ?? 0).toFixed(1),
540
+ cfgMs: +(analysisTiming.cfgMs ?? 0).toFixed(1),
541
+ dataflowMs: +(analysisTiming.dataflowMs ?? 0).toFixed(1),
542
+ finalizeMs: +(p.finalizeMs ?? 0).toFixed(1),
543
+ },
544
+ };
545
+ }
546
+
547
+ /** Try the native build orchestrator. Returns a BuildResult on success, undefined to fall through to JS pipeline. */
548
+ async function tryNativeOrchestrator(
549
+ ctx: PipelineContext,
550
+ ): Promise<BuildResult | undefined | 'early-exit'> {
551
+ const skipReason = shouldSkipNativeOrchestrator(ctx);
552
+ if (skipReason) {
553
+ debug(`Skipping native orchestrator: ${skipReason}`);
554
+ return undefined;
555
+ }
556
+ if (!ctx.nativeDb?.buildGraph) return undefined;
557
+
558
+ const resultJson = ctx.nativeDb.buildGraph(
559
+ ctx.rootDir,
560
+ JSON.stringify(ctx.config),
561
+ JSON.stringify(ctx.aliases),
562
+ JSON.stringify(ctx.opts),
563
+ );
564
+ const result = JSON.parse(resultJson) as NativeOrchestratorResult;
565
+
566
+ if (result.earlyExit) {
567
+ info('No changes detected');
568
+ closeDbPair({ db: ctx.db, nativeDb: ctx.nativeDb });
569
+ return 'early-exit';
570
+ }
571
+
572
+ // Log incremental status to match JS pipeline output
573
+ const changed = result.changedCount ?? 0;
574
+ const removed = result.removedCount ?? 0;
575
+ if (!result.isFullBuild && (changed > 0 || removed > 0)) {
576
+ info(`Incremental: ${changed} changed, ${removed} removed`);
577
+ }
578
+
579
+ const p = result.phases;
580
+
581
+ // Sync build_meta so JS-side version/engine checks work on next build.
582
+ setBuildMeta(ctx.db, {
583
+ engine: ctx.engineName,
584
+ engine_version: ctx.engineVersion || '',
585
+ codegraph_version: CODEGRAPH_VERSION,
586
+ schema_version: String(ctx.schemaVersion),
587
+ built_at: new Date().toISOString(),
588
+ node_count: String(result.nodeCount ?? 0),
589
+ edge_count: String(result.edgeCount ?? 0),
590
+ });
591
+
592
+ info(
593
+ `Native build orchestrator completed: ${result.nodeCount ?? 0} nodes, ${result.edgeCount ?? 0} edges, ${result.fileCount ?? 0} files`,
594
+ );
595
+
596
+ // ── Post-native structure + analysis ──────────────────────────────
597
+ let analysisTiming = { astMs: 0, complexityMs: 0, cfgMs: 0, dataflowMs: 0 };
598
+ let structurePatchMs = 0;
599
+ const needsAnalysis =
600
+ ctx.opts.ast !== false ||
601
+ ctx.opts.complexity !== false ||
602
+ ctx.opts.cfg !== false ||
603
+ ctx.opts.dataflow !== false;
604
+ // Skip JS structure when the Rust pipeline's small-incremental fast path
605
+ // already handled it. For full builds and large incrementals where Rust
606
+ // skipped structure, we must run the JS fallback.
607
+ const needsStructure = !result.structureHandled;
608
+
609
+ if (needsAnalysis || needsStructure) {
610
+ if (!handoffWalAfterNativeBuild(ctx)) {
611
+ // DB reopen failed — return partial result
612
+ return formatNativeTimingResult(p, 0, analysisTiming);
613
+ }
614
+
615
+ // When structure was handled by Rust, we only need changed files for
616
+ // analysis — no need to load the entire graph from DB. When structure
617
+ // was NOT handled, we need all files to build the complete directory tree.
618
+ const scopeFiles = needsStructure ? undefined : result.changedFiles;
619
+ const fileSymbols = reconstructFileSymbolsFromDb(ctx, scopeFiles);
620
+
621
+ if (needsStructure) {
622
+ structurePatchMs = await runPostNativeStructure(
623
+ ctx,
624
+ fileSymbols,
625
+ !!result.isFullBuild,
626
+ result.structureScope ?? result.changedFiles,
627
+ );
628
+ }
629
+
630
+ if (needsAnalysis) {
631
+ analysisTiming = await runPostNativeAnalysis(ctx, fileSymbols, result.changedFiles);
632
+ }
633
+ }
634
+
635
+ closeDbPair({ db: ctx.db, nativeDb: ctx.nativeDb });
636
+ return formatNativeTimingResult(p, structurePatchMs, analysisTiming);
637
+ }
638
+
254
639
  // ── Pipeline stages execution ───────────────────────────────────────────
255
640
 
256
641
  async function runPipelineStages(ctx: PipelineContext): Promise<void> {
@@ -338,368 +723,13 @@ export async function buildGraph(
338
723
  // When available, run the entire build pipeline in Rust with zero
339
724
  // napi crossings (eliminates WAL dual-connection dance). Falls back
340
725
  // to the JS pipeline on failure or when native is unavailable.
341
- //
342
- // Native addon ≤3.8.0 has a path bug: file_symbols keys are absolute
343
- // paths but known_files are relative, causing zero import/call edges.
344
- // Native addon ≤3.8.1 has an incremental barrel bug: the Rust pipeline
345
- // doesn't re-parse barrel files that are imported by changed files,
346
- // causing missing barrel import edges and lost analysis data for
347
- // reverse-dep files during incremental builds.
348
- // Skip the orchestrator for affected versions (fixed in 3.9.0+).
349
- const orchestratorBuggy = !!ctx.engineVersion && semverCompare(ctx.engineVersion, '3.8.1') <= 0;
350
- const forceJs =
351
- process.env.CODEGRAPH_FORCE_JS_PIPELINE === '1' ||
352
- ctx.forceFullRebuild ||
353
- orchestratorBuggy ||
354
- ctx.engineName !== 'native';
355
- if (forceJs) {
356
- const reason =
357
- process.env.CODEGRAPH_FORCE_JS_PIPELINE === '1'
358
- ? 'CODEGRAPH_FORCE_JS_PIPELINE=1'
359
- : ctx.forceFullRebuild
360
- ? 'forceFullRebuild'
361
- : orchestratorBuggy
362
- ? `buggy addon ${ctx.engineVersion}`
363
- : `engine=${ctx.engineName}`;
364
- debug(`Skipping native orchestrator: ${reason}`);
365
- }
366
- if (!forceJs && ctx.nativeDb?.buildGraph) {
367
- try {
368
- const resultJson = ctx.nativeDb.buildGraph(
369
- ctx.rootDir,
370
- JSON.stringify(ctx.config),
371
- JSON.stringify(ctx.aliases),
372
- JSON.stringify(opts),
373
- );
374
- const result = JSON.parse(resultJson) as {
375
- phases: Record<string, number>;
376
- earlyExit?: boolean;
377
- nodeCount?: number;
378
- edgeCount?: number;
379
- fileCount?: number;
380
- changedFiles?: string[];
381
- changedCount?: number;
382
- removedCount?: number;
383
- isFullBuild?: boolean;
384
- };
385
-
386
- if (result.earlyExit) {
387
- info('No changes detected');
388
- closeDbPair({ db: ctx.db, nativeDb: ctx.nativeDb });
389
- return;
390
- }
391
-
392
- // Log incremental status to match JS pipeline output
393
- const changed = result.changedCount ?? 0;
394
- const removed = result.removedCount ?? 0;
395
- if (!result.isFullBuild && (changed > 0 || removed > 0)) {
396
- info(`Incremental: ${changed} changed, ${removed} removed`);
397
- }
398
-
399
- // Map Rust timing fields to the JS BuildResult format.
400
- // Rust handles collect+detect+parse+insert+resolve+edges+structure+roles.
401
- const p = result.phases;
402
-
403
- // Sync build_meta so JS-side version/engine checks work on next build.
404
- // Note: the Rust orchestrator also writes codegraph_version (using
405
- // CARGO_PKG_VERSION). We intentionally overwrite it here with the npm
406
- // package version so that the JS-side "version changed → full rebuild"
407
- // detection (line ~97) compares against the authoritative JS version.
408
- // The two versions are kept in lockstep by the release process.
409
- setBuildMeta(ctx.db, {
410
- engine: ctx.engineName,
411
- engine_version: ctx.engineVersion || '',
412
- codegraph_version: CODEGRAPH_VERSION,
413
- schema_version: String(ctx.schemaVersion),
414
- built_at: new Date().toISOString(),
415
- node_count: String(result.nodeCount ?? 0),
416
- edge_count: String(result.edgeCount ?? 0),
417
- });
418
-
419
- info(
420
- `Native build orchestrator completed: ${result.nodeCount ?? 0} nodes, ${result.edgeCount ?? 0} edges, ${result.fileCount ?? 0} files`,
421
- );
422
-
423
- // ── Run structure + analysis phases after native orchestrator ──
424
- // Structure (directory nodes, contains edges, metrics) is not fully
425
- // ported to Rust — the native pipeline only handles the small
426
- // incremental fast path (≤5 changed files). For full builds and
427
- // larger incremental builds, run JS buildStructure() to fill the gap.
428
- // Analysis phases (AST, complexity, CFG, dataflow) are also not yet
429
- // ported; run via JS engine after reconstructing fileSymbols from DB.
430
- let analysisTiming = { astMs: 0, complexityMs: 0, cfgMs: 0, dataflowMs: 0 };
431
- let structurePatchMs = 0;
432
- const needsAnalysis =
433
- opts.ast !== false ||
434
- opts.complexity !== false ||
435
- opts.cfg !== false ||
436
- opts.dataflow !== false;
437
-
438
- // The native fast path only runs structure for small incremental
439
- // builds: !isFullBuild && changedCount <= 5 && existingFileCount > 20.
440
- // For all other cases (full builds, large incrementals), we must
441
- // run JS buildStructure() to create directory nodes + contains edges (#804).
442
- // Always run JS structure — the native fast-path has an additional
443
- // existingFileCount > 20 guard that isn't reflected in the result JSON,
444
- // so we can't reliably detect whether native actually ran structure.
445
- const nativeHandledStructure = false;
446
- const needsStructure = !nativeHandledStructure;
447
-
448
- if (needsAnalysis || needsStructure) {
449
- // WAL handoff: checkpoint through rusqlite, close nativeDb,
450
- // reopen better-sqlite3 with a fresh page cache (#715, #736).
451
- try {
452
- ctx.nativeDb!.exec('PRAGMA wal_checkpoint(TRUNCATE)');
453
- } catch {
454
- /* ignore checkpoint errors */
455
- }
456
- try {
457
- ctx.nativeDb!.close();
458
- } catch {
459
- /* ignore close errors */
460
- }
461
- ctx.nativeDb = undefined;
462
- try {
463
- ctx.db.close();
464
- } catch {
465
- /* ignore close errors */
466
- }
467
- ctx.db = null!; // avoid closeDbPair operating on a stale handle
468
- try {
469
- ctx.db = openDb(ctx.dbPath);
470
- } catch (reopenErr) {
471
- warn(`Failed to reopen DB after native build: ${(reopenErr as Error).message}`);
472
- // Native build succeeded but we can't run post-processing — return partial result
473
- return {
474
- phases: {
475
- setupMs: +((p.setupMs ?? 0) + (p.collectMs ?? 0) + (p.detectMs ?? 0)).toFixed(1),
476
- parseMs: +(p.parseMs ?? 0).toFixed(1),
477
- insertMs: +(p.insertMs ?? 0).toFixed(1),
478
- resolveMs: +(p.resolveMs ?? 0).toFixed(1),
479
- edgesMs: +(p.edgesMs ?? 0).toFixed(1),
480
- structureMs: +(p.structureMs ?? 0).toFixed(1),
481
- rolesMs: +(p.rolesMs ?? 0).toFixed(1),
482
- astMs: 0,
483
- complexityMs: 0,
484
- cfgMs: 0,
485
- dataflowMs: 0,
486
- finalizeMs: +(p.finalizeMs ?? 0).toFixed(1),
487
- },
488
- };
489
- }
490
-
491
- // Reconstruct fileSymbols from DB. For structure we need ALL files
492
- // (to build complete directory tree); for analysis we scope to
493
- // changed files only. Load all files, then scope analysis later.
494
- const allFileRows = ctx.db
495
- .prepare(
496
- 'SELECT file, name, kind, line, end_line as endLine FROM nodes WHERE file IS NOT NULL ORDER BY file, line',
497
- )
498
- .all() as {
499
- file: string;
500
- name: string;
501
- kind: string;
502
- line: number;
503
- endLine: number | null;
504
- }[];
505
-
506
- const allFileSymbols = new Map<string, ExtractorOutput>();
507
- for (const row of allFileRows) {
508
- let entry = allFileSymbols.get(row.file);
509
- if (!entry) {
510
- entry = {
511
- definitions: [],
512
- calls: [],
513
- imports: [],
514
- classes: [],
515
- exports: [],
516
- typeMap: new Map(),
517
- };
518
- allFileSymbols.set(row.file, entry);
519
- }
520
- entry.definitions.push({
521
- name: row.name,
522
- kind: row.kind as Definition['kind'],
523
- line: row.line,
524
- endLine: row.endLine ?? undefined,
525
- });
526
- }
527
-
528
- // Populate import/export counts from DB edges so buildStructure
529
- // computes correct import_count/export_count in node_metrics.
530
- // The extractor arrays aren't persisted to the DB, so we derive
531
- // counts from edge data instead (#804).
532
- const importCountRows = ctx.db
533
- .prepare(
534
- `SELECT n.file, COUNT(*) AS cnt
535
- FROM edges e JOIN nodes n ON e.source_id = n.id
536
- WHERE e.kind IN ('imports', 'imports-type', 'dynamic-imports')
537
- AND n.file IS NOT NULL
538
- GROUP BY n.file`,
539
- )
540
- .all() as { file: string; cnt: number }[];
541
- for (const row of importCountRows) {
542
- const entry = allFileSymbols.get(row.file);
543
- if (entry) entry.imports = new Array(row.cnt) as ExtractorOutput['imports'];
544
- }
545
- // Export count: definitions in this file that are imported by other files
546
- const exportCountRows = ctx.db
547
- .prepare(
548
- `SELECT n_tgt.file, COUNT(DISTINCT n_tgt.id) AS cnt
549
- FROM edges e
550
- JOIN nodes n_tgt ON e.target_id = n_tgt.id
551
- JOIN nodes n_src ON e.source_id = n_src.id
552
- WHERE e.kind IN ('imports', 'imports-type', 'reexports')
553
- AND n_tgt.file IS NOT NULL
554
- AND n_src.file != n_tgt.file
555
- GROUP BY n_tgt.file`,
556
- )
557
- .all() as { file: string; cnt: number }[];
558
- for (const row of exportCountRows) {
559
- const entry = allFileSymbols.get(row.file);
560
- if (entry) entry.exports = new Array(row.cnt) as ExtractorOutput['exports'];
561
- }
562
-
563
- // ── Structure phase: directory nodes + contains edges (#804) ──
564
- if (needsStructure) {
565
- const structureStart = performance.now();
566
- try {
567
- // Derive directories from file paths
568
- const directories = new Set<string>();
569
- for (const relPath of allFileSymbols.keys()) {
570
- const parts = relPath.split('/');
571
- for (let i = 1; i < parts.length; i++) {
572
- directories.add(parts.slice(0, i).join('/'));
573
- }
574
- }
575
-
576
- // Build line count map from DB metrics or file content
577
- const lineCountMap = new Map<string, number>();
578
- const cachedLineCounts = ctx.db
579
- .prepare(
580
- `SELECT n.name AS file, m.line_count
581
- FROM node_metrics m JOIN nodes n ON m.node_id = n.id
582
- WHERE n.kind = 'file'`,
583
- )
584
- .all() as Array<{ file: string; line_count: number }>;
585
- for (const row of cachedLineCounts) {
586
- lineCountMap.set(row.file, row.line_count);
587
- }
588
-
589
- // Native ran no structure at all — always do a full rebuild so
590
- // every directory gets nodes + contains edges (#804).
591
- const changedFilePaths = null;
592
-
593
- const { buildStructure: buildStructureFn } = (await import(
594
- '../../../features/structure.js'
595
- )) as {
596
- buildStructure: (
597
- db: typeof ctx.db,
598
- fileSymbols: Map<string, ExtractorOutput>,
599
- rootDir: string,
600
- lineCountMap: Map<string, number>,
601
- directories: Set<string>,
602
- changedFiles: string[] | null,
603
- ) => void;
604
- };
605
- buildStructureFn(
606
- ctx.db,
607
- allFileSymbols,
608
- ctx.rootDir,
609
- lineCountMap,
610
- directories,
611
- changedFilePaths,
612
- );
613
- debug('Structure phase completed after native orchestrator');
614
- } catch (err) {
615
- warn(`Structure phase failed after native build: ${toErrorMessage(err)}`);
616
- }
617
- structurePatchMs = performance.now() - structureStart;
618
- }
619
-
620
- // ── Analysis phase ──
621
- if (needsAnalysis) {
622
- // Scope analysis fileSymbols to changed files only
623
- const changedFiles = result.changedFiles;
624
- let analysisFileSymbols: Map<string, ExtractorOutput>;
625
- if (changedFiles && changedFiles.length > 0) {
626
- analysisFileSymbols = new Map();
627
- for (const f of changedFiles) {
628
- const entry = allFileSymbols.get(f);
629
- if (entry) analysisFileSymbols.set(f, entry);
630
- }
631
- } else {
632
- analysisFileSymbols = allFileSymbols;
633
- }
634
-
635
- // Reopen nativeDb for analysis features (suspend/resume WAL pattern).
636
- const native = loadNative();
637
- if (native?.NativeDatabase) {
638
- try {
639
- ctx.nativeDb = native.NativeDatabase.openReadWrite(ctx.dbPath);
640
- if (ctx.engineOpts) ctx.engineOpts.nativeDb = ctx.nativeDb;
641
- } catch {
642
- ctx.nativeDb = undefined;
643
- if (ctx.engineOpts) ctx.engineOpts.nativeDb = undefined;
644
- }
645
- }
646
-
647
- try {
648
- const { runAnalyses: runAnalysesFn } = await import(
649
- '../../../ast-analysis/engine.js'
650
- );
651
- analysisTiming = await runAnalysesFn(
652
- ctx.db,
653
- analysisFileSymbols,
654
- ctx.rootDir,
655
- opts,
656
- ctx.engineOpts,
657
- );
658
- } catch (err) {
659
- warn(`Analysis phases failed after native build: ${toErrorMessage(err)}`);
660
- }
661
-
662
- // Close nativeDb after analyses
663
- if (ctx.nativeDb) {
664
- try {
665
- ctx.nativeDb.exec('PRAGMA wal_checkpoint(TRUNCATE)');
666
- } catch {
667
- /* ignore checkpoint errors */
668
- }
669
- try {
670
- ctx.nativeDb.close();
671
- } catch {
672
- /* ignore close errors */
673
- }
674
- ctx.nativeDb = undefined;
675
- if (ctx.engineOpts) ctx.engineOpts.nativeDb = undefined;
676
- }
677
- }
678
- }
679
-
680
- closeDbPair({ db: ctx.db, nativeDb: ctx.nativeDb });
681
- return {
682
- phases: {
683
- setupMs: +((p.setupMs ?? 0) + (p.collectMs ?? 0) + (p.detectMs ?? 0)).toFixed(1),
684
- parseMs: +(p.parseMs ?? 0).toFixed(1),
685
- insertMs: +(p.insertMs ?? 0).toFixed(1),
686
- resolveMs: +(p.resolveMs ?? 0).toFixed(1),
687
- edgesMs: +(p.edgesMs ?? 0).toFixed(1),
688
- structureMs: +((p.structureMs ?? 0) + structurePatchMs).toFixed(1),
689
- rolesMs: +(p.rolesMs ?? 0).toFixed(1),
690
- astMs: +(analysisTiming.astMs ?? 0).toFixed(1),
691
- complexityMs: +(analysisTiming.complexityMs ?? 0).toFixed(1),
692
- cfgMs: +(analysisTiming.cfgMs ?? 0).toFixed(1),
693
- dataflowMs: +(analysisTiming.dataflowMs ?? 0).toFixed(1),
694
- finalizeMs: +(p.finalizeMs ?? 0).toFixed(1),
695
- },
696
- };
697
- } catch (err) {
698
- warn(
699
- `Native build orchestrator failed, falling back to JS pipeline: ${toErrorMessage(err)}`,
700
- );
701
- // Fall through to JS pipeline
702
- }
726
+ try {
727
+ const nativeResult = await tryNativeOrchestrator(ctx);
728
+ if (nativeResult === 'early-exit') return;
729
+ if (nativeResult) return nativeResult;
730
+ } catch (err) {
731
+ warn(`Native build orchestrator failed, falling back to JS pipeline: ${toErrorMessage(err)}`);
732
+ // Fall through to JS pipeline
703
733
  }
704
734
 
705
735
  await runPipelineStages(ctx);