@optave/codegraph 3.9.2 → 3.9.3

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 (42) hide show
  1. package/README.md +93 -10
  2. package/dist/ast-analysis/engine.d.ts.map +1 -1
  3. package/dist/ast-analysis/engine.js +64 -0
  4. package/dist/ast-analysis/engine.js.map +1 -1
  5. package/dist/domain/analysis/diff-impact.d.ts +12 -0
  6. package/dist/domain/analysis/diff-impact.d.ts.map +1 -1
  7. package/dist/domain/analysis/diff-impact.js +20 -1
  8. package/dist/domain/analysis/diff-impact.js.map +1 -1
  9. package/dist/domain/graph/builder/native-db-proxy.d.ts.map +1 -1
  10. package/dist/domain/graph/builder/native-db-proxy.js +8 -4
  11. package/dist/domain/graph/builder/native-db-proxy.js.map +1 -1
  12. package/dist/domain/graph/builder/pipeline.d.ts.map +1 -1
  13. package/dist/domain/graph/builder/pipeline.js +89 -84
  14. package/dist/domain/graph/builder/pipeline.js.map +1 -1
  15. package/dist/domain/parser.d.ts.map +1 -1
  16. package/dist/domain/parser.js +6 -2
  17. package/dist/domain/parser.js.map +1 -1
  18. package/dist/features/ast.js +2 -2
  19. package/dist/features/ast.js.map +1 -1
  20. package/dist/features/cfg.d.ts +1 -1
  21. package/dist/features/cfg.d.ts.map +1 -1
  22. package/dist/features/cfg.js +52 -6
  23. package/dist/features/cfg.js.map +1 -1
  24. package/dist/features/complexity.d.ts.map +1 -1
  25. package/dist/features/complexity.js +7 -0
  26. package/dist/features/complexity.js.map +1 -1
  27. package/dist/infrastructure/update-check.d.ts +1 -1
  28. package/dist/infrastructure/update-check.js +3 -3
  29. package/dist/infrastructure/update-check.js.map +1 -1
  30. package/dist/types.d.ts +1 -0
  31. package/dist/types.d.ts.map +1 -1
  32. package/package.json +7 -7
  33. package/src/ast-analysis/engine.ts +83 -0
  34. package/src/domain/analysis/diff-impact.ts +28 -1
  35. package/src/domain/graph/builder/native-db-proxy.ts +10 -4
  36. package/src/domain/graph/builder/pipeline.ts +108 -89
  37. package/src/domain/parser.ts +6 -2
  38. package/src/features/ast.ts +2 -2
  39. package/src/features/cfg.ts +51 -6
  40. package/src/features/complexity.ts +7 -0
  41. package/src/infrastructure/update-check.ts +3 -3
  42. package/src/types.ts +1 -0
@@ -23,7 +23,13 @@ import { loadNative } from '../../../infrastructure/native.js';
23
23
  import { semverCompare } from '../../../infrastructure/update-check.js';
24
24
  import { toErrorMessage } from '../../../shared/errors.js';
25
25
  import { CODEGRAPH_VERSION } from '../../../shared/version.js';
26
- import type { BuildGraphOpts, BuildResult, Definition, ExtractorOutput } from '../../../types.js';
26
+ import type {
27
+ BetterSqlite3Database,
28
+ BuildGraphOpts,
29
+ BuildResult,
30
+ Definition,
31
+ ExtractorOutput,
32
+ } from '../../../types.js';
27
33
  import { getActiveEngine } from '../../parser.js';
28
34
  import { setWorkspaces } from '../resolve.js';
29
35
  import { PipelineContext } from './context.js';
@@ -120,15 +126,11 @@ function setupPipeline(ctx: PipelineContext): void {
120
126
  const native = enginePref !== 'wasm' ? loadNative() : null;
121
127
  ctx.nativeAvailable = !!native?.NativeDatabase;
122
128
 
123
- // Native-first: use only rusqlite for the entire pipeline (no better-sqlite3).
124
- // This eliminates the dual-connection WAL corruption problem and enables all
125
- // native fast-paths (bulkInsertNodes, classifyRolesFull, etc.).
126
- // Fallback: if native is unavailable or FORCE_JS is set, use better-sqlite3.
127
- if (
128
- ctx.nativeAvailable &&
129
- native?.NativeDatabase &&
130
- process.env.CODEGRAPH_FORCE_JS_PIPELINE !== '1'
131
- ) {
129
+ // When native is available, use a NativeDbProxy backed by a single rusqlite
130
+ // connection. This eliminates the dual-connection WAL corruption problem.
131
+ // The Rust orchestrator handles the full pipeline; the proxy is used for any
132
+ // JS post-processing (e.g. structure fallback on large builds).
133
+ if (ctx.nativeAvailable && native?.NativeDatabase) {
132
134
  try {
133
135
  const dir = path.dirname(ctx.dbPath);
134
136
  if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
@@ -264,13 +266,14 @@ interface NativeOrchestratorResult {
264
266
  structureScope?: string[];
265
267
  /** Whether the Rust pipeline handled the structure phase (small-incremental fast path). */
266
268
  structureHandled?: boolean;
269
+ /** Whether the Rust pipeline wrote AST/complexity/CFG/dataflow to DB. */
270
+ analysisComplete?: boolean;
267
271
  }
268
272
 
269
273
  // ── Native orchestrator helpers ───────────────────────────────────────
270
274
 
271
275
  /** Determine whether the native orchestrator should be skipped. Returns a reason string, or null if it should run. */
272
276
  function shouldSkipNativeOrchestrator(ctx: PipelineContext): string | null {
273
- if (process.env.CODEGRAPH_FORCE_JS_PIPELINE === '1') return 'CODEGRAPH_FORCE_JS_PIPELINE=1';
274
277
  if (ctx.forceFullRebuild) return 'forceFullRebuild';
275
278
  // v3.9.0 addon had buggy incremental purge (wrong SQL on analysis tables,
276
279
  // scoped removal over-detection). Fixed in v3.9.1 by PR #865. Gate on
@@ -452,7 +455,11 @@ async function runPostNativeStructure(
452
455
  return performance.now() - structureStart;
453
456
  }
454
457
 
455
- /** Run AST/complexity/CFG/dataflow analysis after native orchestrator. */
458
+ /**
459
+ * JS fallback for AST/complexity/CFG/dataflow analysis after native orchestrator.
460
+ * Used when the Rust addon doesn't include analysis persistence (older addon
461
+ * version) or when analysis failed on the Rust side.
462
+ */
456
463
  async function runPostNativeAnalysis(
457
464
  ctx: PipelineContext,
458
465
  allFileSymbols: Map<string, ExtractorOutput>,
@@ -472,30 +479,43 @@ async function runPostNativeAnalysis(
472
479
  analysisFileSymbols = allFileSymbols;
473
480
  }
474
481
 
475
- // In native-first mode, nativeDb is already open no reopen needed.
476
- if (!ctx.nativeFirstProxy) {
477
- const native = loadNative();
478
- if (native?.NativeDatabase) {
479
- try {
480
- ctx.nativeDb = native.NativeDatabase.openReadWrite(ctx.dbPath);
481
- if (ctx.engineOpts) ctx.engineOpts.nativeDb = ctx.nativeDb;
482
- } catch {
483
- ctx.nativeDb = undefined;
484
- if (ctx.engineOpts) ctx.engineOpts.nativeDb = undefined;
485
- }
482
+ // Reopen nativeDb for analysis features (suspend/resume WAL pattern).
483
+ const native = loadNative();
484
+ if (native?.NativeDatabase) {
485
+ try {
486
+ ctx.nativeDb = native.NativeDatabase.openReadWrite(ctx.dbPath);
487
+ if (ctx.engineOpts) ctx.engineOpts.nativeDb = ctx.nativeDb;
488
+ } catch {
489
+ ctx.nativeDb = undefined;
490
+ if (ctx.engineOpts) ctx.engineOpts.nativeDb = undefined;
486
491
  }
487
- } else if (ctx.engineOpts) {
488
- ctx.engineOpts.nativeDb = ctx.nativeDb;
492
+ }
493
+
494
+ // Flush JS WAL pages once so Rust can see them, then no-op callbacks.
495
+ // Previously each feature called wal_checkpoint(TRUNCATE) individually
496
+ // (~68ms each × 3-4 features). One FULL checkpoint suffices.
497
+ if (ctx.nativeDb && ctx.engineOpts) {
498
+ ctx.db.pragma('wal_checkpoint(FULL)');
499
+ ctx.engineOpts.suspendJsDb = () => {};
500
+ ctx.engineOpts.resumeJsDb = () => {};
489
501
  }
490
502
 
491
503
  try {
492
- const { runAnalyses: runAnalysesFn } = await import('../../../ast-analysis/engine.js');
504
+ const { runAnalyses: runAnalysesFn } = (await import('../../../ast-analysis/engine.js')) as {
505
+ runAnalyses: (
506
+ db: BetterSqlite3Database,
507
+ fileSymbols: Map<string, ExtractorOutput>,
508
+ rootDir: string,
509
+ opts: Record<string, unknown>,
510
+ engineOpts?: Record<string, unknown>,
511
+ ) => Promise<{ astMs?: number; complexityMs?: number; cfgMs?: number; dataflowMs?: number }>;
512
+ };
493
513
  const result = await runAnalysesFn(
494
514
  ctx.db,
495
515
  analysisFileSymbols,
496
516
  ctx.rootDir,
497
- ctx.opts,
498
- ctx.engineOpts,
517
+ ctx.opts as Record<string, unknown>,
518
+ ctx.engineOpts as unknown as Record<string, unknown> | undefined,
499
519
  );
500
520
  timing.astMs = result.astMs ?? 0;
501
521
  timing.complexityMs = result.complexityMs ?? 0;
@@ -505,8 +525,10 @@ async function runPostNativeAnalysis(
505
525
  warn(`Analysis phases failed after native build: ${toErrorMessage(err)}`);
506
526
  }
507
527
 
508
- // Close nativeDb after analyses (skip in native-first single connection stays open)
509
- if (ctx.nativeDb && !ctx.nativeFirstProxy) {
528
+ // Close nativeDb after analyses TRUNCATE checkpoint flushes all Rust
529
+ // WAL writes so JS and external readers can see them. Runs once after
530
+ // all analysis features complete (not per-feature).
531
+ if (ctx.nativeDb) {
510
532
  try {
511
533
  ctx.nativeDb.exec('PRAGMA wal_checkpoint(TRUNCATE)');
512
534
  } catch {
@@ -518,7 +540,11 @@ async function runPostNativeAnalysis(
518
540
  /* ignore close errors */
519
541
  }
520
542
  ctx.nativeDb = undefined;
521
- if (ctx.engineOpts) ctx.engineOpts.nativeDb = undefined;
543
+ if (ctx.engineOpts) {
544
+ ctx.engineOpts.nativeDb = undefined;
545
+ ctx.engineOpts.suspendJsDb = undefined;
546
+ ctx.engineOpts.resumeJsDb = undefined;
547
+ }
522
548
  }
523
549
 
524
550
  return timing;
@@ -620,30 +646,40 @@ async function tryNativeOrchestrator(
620
646
  );
621
647
 
622
648
  // ── Post-native structure + analysis ──────────────────────────────
623
- let analysisTiming = { astMs: 0, complexityMs: 0, cfgMs: 0, dataflowMs: 0 };
649
+ let analysisTiming = {
650
+ astMs: +(p.astMs ?? 0),
651
+ complexityMs: +(p.complexityMs ?? 0),
652
+ cfgMs: +(p.cfgMs ?? 0),
653
+ dataflowMs: +(p.dataflowMs ?? 0),
654
+ };
624
655
  let structurePatchMs = 0;
625
- const needsAnalysis =
626
- ctx.opts.ast !== false ||
627
- ctx.opts.complexity !== false ||
628
- ctx.opts.cfg !== false ||
629
- ctx.opts.dataflow !== false;
630
656
  // Skip JS structure when the Rust pipeline's small-incremental fast path
631
657
  // already handled it. For full builds and large incrementals where Rust
632
658
  // skipped structure, we must run the JS fallback.
633
659
  const needsStructure = !result.structureHandled;
634
-
635
- if (needsAnalysis || needsStructure) {
636
- // In native-first mode the proxy is already wired — no WAL handoff needed.
637
- if (!ctx.nativeFirstProxy && !handoffWalAfterNativeBuild(ctx)) {
660
+ // When the Rust addon doesn't include analysis persistence (older addon
661
+ // version or analysis failed), fall back to JS-side analysis.
662
+ const needsAnalysisFallback =
663
+ !result.analysisComplete &&
664
+ (ctx.opts.ast !== false ||
665
+ ctx.opts.complexity !== false ||
666
+ ctx.opts.cfg !== false ||
667
+ ctx.opts.dataflow !== false);
668
+
669
+ if (needsStructure || needsAnalysisFallback) {
670
+ // When analysis fallback is needed, handoff to better-sqlite3 — the
671
+ // analysis engine uses the suspend/resume WAL pattern that requires a
672
+ // real better-sqlite3 connection, not the NativeDbProxy.
673
+ if (needsAnalysisFallback && ctx.nativeFirstProxy) {
674
+ closeNativeDb(ctx, 'pre-analysis-fallback');
675
+ ctx.db = openDb(ctx.dbPath);
676
+ ctx.nativeFirstProxy = false;
677
+ } else if (!ctx.nativeFirstProxy && !handoffWalAfterNativeBuild(ctx)) {
638
678
  // DB reopen failed — return partial result
639
679
  return formatNativeTimingResult(p, 0, analysisTiming);
640
680
  }
641
681
 
642
- // When structure was handled by Rust, we only need changed files for
643
- // analysis — no need to load the entire graph from DB. When structure
644
- // was NOT handled, we need all files to build the complete directory tree.
645
- const scopeFiles = needsStructure ? undefined : result.changedFiles;
646
- const fileSymbols = reconstructFileSymbolsFromDb(ctx, scopeFiles);
682
+ const fileSymbols = reconstructFileSymbolsFromDb(ctx);
647
683
 
648
684
  if (needsStructure) {
649
685
  structurePatchMs = await runPostNativeStructure(
@@ -654,7 +690,7 @@ async function tryNativeOrchestrator(
654
690
  );
655
691
  }
656
692
 
657
- if (needsAnalysis) {
693
+ if (needsAnalysisFallback) {
658
694
  analysisTiming = await runPostNativeAnalysis(ctx, fileSymbols, result.changedFiles);
659
695
  }
660
696
  }
@@ -666,30 +702,7 @@ async function tryNativeOrchestrator(
666
702
  // ── Pipeline stages execution ───────────────────────────────────────────
667
703
 
668
704
  async function runPipelineStages(ctx: PipelineContext): Promise<void> {
669
- // ── Native-first mode ────────────────────────────────────────────────
670
- // When ctx.nativeFirstProxy is true, ctx.db is a NativeDbProxy backed by
671
- // the single rusqlite connection (ctx.nativeDb). No dual-connection WAL
672
- // dance is needed — every stage uses the same connection transparently.
673
- if (ctx.nativeFirstProxy) {
674
- // Ensure engineOpts.nativeDb is set so stages can use dedicated native methods.
675
- if (ctx.engineOpts) {
676
- ctx.engineOpts.nativeDb = ctx.nativeDb;
677
- }
678
-
679
- await collectFiles(ctx);
680
- await detectChanges(ctx);
681
- if (ctx.earlyExit) return;
682
- await parseFiles(ctx);
683
- await insertNodes(ctx);
684
- await resolveImports(ctx);
685
- await buildEdges(ctx);
686
- await buildStructure(ctx);
687
- await runAnalyses(ctx);
688
- await finalize(ctx);
689
- return;
690
- }
691
-
692
- // ── Legacy dual-connection mode (WASM / fallback) ────────────────────
705
+ // ── WASM / fallback dual-connection mode ─────────────────────────────
693
706
  // NativeDatabase is deferred — not opened during setup. collectFiles and
694
707
  // detectChanges only need better-sqlite3. If no files changed, we exit
695
708
  // early without ever opening the native connection, saving ~5ms.
@@ -697,6 +710,13 @@ async function runPipelineStages(ctx: PipelineContext): Promise<void> {
697
710
  // suspend it now to avoid dual-connection WAL corruption during stages.
698
711
  if (ctx.db && ctx.nativeDb) {
699
712
  suspendNativeDb(ctx, 'pre-collect');
713
+ // When nativeFirstProxy is true, ctx.db is a NativeDbProxy wrapping the
714
+ // now-closed NativeDatabase. Replace it with a real better-sqlite3
715
+ // connection so the JS pipeline stages can operate normally.
716
+ if (ctx.nativeFirstProxy) {
717
+ ctx.db = openDb(ctx.dbPath);
718
+ ctx.nativeFirstProxy = false;
719
+ }
700
720
  }
701
721
 
702
722
  await collectFiles(ctx);
@@ -728,25 +748,20 @@ async function runPipelineStages(ctx: PipelineContext): Promise<void> {
728
748
  await buildEdges(ctx);
729
749
  await buildStructure(ctx);
730
750
 
731
- // Reopen nativeDb for feature modules (ast, cfg, complexity, dataflow)
732
- // which use suspendJsDb/resumeJsDb WAL checkpoint before native writes.
751
+ // Reopen nativeDb for feature modules (ast, cfg, complexity, dataflow).
733
752
  // Skip for small incremental builds — same rationale as insertNodes above.
753
+ //
754
+ // Perf: do ONE upfront FULL checkpoint to flush JS WAL pages so Rust
755
+ // can see the latest rows, then make suspendJsDb/resumeJsDb no-ops.
756
+ // Previously each feature called wal_checkpoint(TRUNCATE) individually
757
+ // (~68ms each × 3-4 features = ~200-270ms overhead on incremental builds).
734
758
  if (ctx.nativeAvailable && !smallIncremental) {
735
759
  reopenNativeDb(ctx, 'analyses');
736
760
  if (ctx.nativeDb && ctx.engineOpts) {
761
+ ctx.db.pragma('wal_checkpoint(FULL)');
737
762
  ctx.engineOpts.nativeDb = ctx.nativeDb;
738
- ctx.engineOpts.suspendJsDb = () => {
739
- ctx.db.pragma('wal_checkpoint(TRUNCATE)');
740
- };
741
- ctx.engineOpts.resumeJsDb = () => {
742
- try {
743
- ctx.nativeDb?.exec('PRAGMA wal_checkpoint(TRUNCATE)');
744
- } catch (e) {
745
- debug(
746
- `resumeJsDb: WAL checkpoint failed (nativeDb may already be closed): ${toErrorMessage(e)}`,
747
- );
748
- }
749
- };
763
+ ctx.engineOpts.suspendJsDb = () => {};
764
+ ctx.engineOpts.resumeJsDb = () => {};
750
765
  }
751
766
  if (!ctx.nativeDb && ctx.engineOpts) {
752
767
  ctx.engineOpts.nativeDb = undefined;
@@ -757,11 +772,15 @@ async function runPipelineStages(ctx: PipelineContext): Promise<void> {
757
772
 
758
773
  await runAnalyses(ctx);
759
774
 
760
- // Keep nativeDb open through finalize so persistBuildMetadata, advisory
761
- // checks, and count queries use the native path. closeDbPair inside
762
- // finalize handles both connections. Refresh the JS db so it has a
763
- // valid page cache in case finalize falls back to JS paths (#751).
775
+ // Flush Rust WAL writes (AST, complexity, CFG, dataflow) so the JS
776
+ // connection and any post-build readers can see them. One TRUNCATE
777
+ // here replaces the N per-feature resumeJsDb checkpoints (#checkpoint-opt).
764
778
  if (ctx.nativeDb) {
779
+ try {
780
+ ctx.nativeDb.exec('PRAGMA wal_checkpoint(TRUNCATE)');
781
+ } catch (e) {
782
+ debug(`post-analyses WAL checkpoint failed: ${toErrorMessage(e)}`);
783
+ }
765
784
  refreshJsDb(ctx);
766
785
  }
767
786
 
@@ -780,7 +780,7 @@ export async function parseFileAuto(
780
780
  const { native } = resolveEngine(opts);
781
781
 
782
782
  if (native) {
783
- const result = native.parseFile(filePath, source, !!opts.dataflow, opts.ast !== false);
783
+ const result = native.parseFile(filePath, source, true, true);
784
784
  if (!result) return null;
785
785
  const patched = patchNativeResult(result);
786
786
  // Always backfill typeMap for TS/TSX from WASM — native parser's type
@@ -878,7 +878,11 @@ export async function parseFilesAuto(
878
878
  if (!native) return parseFilesWasm(filePaths, rootDir);
879
879
 
880
880
  const result = new Map<string, ExtractorOutput>();
881
- const nativeResults = native.parseFiles(filePaths, rootDir, !!opts.dataflow, opts.ast !== false);
881
+ // Always extract all analysis data (dataflow + AST nodes) during native parse.
882
+ // This eliminates the need for any downstream WASM re-parse or native standalone calls.
883
+ const nativeResults = native.parseFilesFull
884
+ ? native.parseFilesFull(filePaths, rootDir)
885
+ : native.parseFiles(filePaths, rootDir, true, true);
882
886
  const needsTypeMap: { filePath: string; relPath: string }[] = [];
883
887
  for (const r of nativeResults) {
884
888
  if (!r) continue;
@@ -115,8 +115,8 @@ function tryNativeBulkInsert(
115
115
  receiver: n.receiver ?? '',
116
116
  })),
117
117
  });
118
- } else if (symbols.calls || symbols._tree) {
119
- return false; // needs JS fallback
118
+ } else if (symbols._tree) {
119
+ return false; // has WASM tree not yet processed — needs JS fallback
120
120
  }
121
121
  }
122
122
 
@@ -369,7 +369,7 @@ export async function buildCFGData(
369
369
  db: BetterSqlite3Database,
370
370
  fileSymbols: Map<string, FileSymbols>,
371
371
  rootDir: string,
372
- _engineOpts?: {
372
+ engineOpts?: {
373
373
  nativeDb?: { bulkInsertCfg?(entries: Array<Record<string, unknown>>): number };
374
374
  suspendJsDb?: () => void;
375
375
  resumeJsDb?: () => void;
@@ -379,11 +379,56 @@ export async function buildCFGData(
379
379
  // skip WASM parser init, tree parsing, and JS visitor entirely — just persist.
380
380
  const allNative = allCfgNative(fileSymbols);
381
381
 
382
- // NOTE: nativeDb.bulkInsertCfg is intentionally NOT used here.
383
- // The CFG path requires delete-before-insert (deleteCfgForNode) which creates
384
- // a dual-connection WAL conflict when deletes go through JS (better-sqlite3)
385
- // and inserts go through native (rusqlite). The JS-only persistNativeFileCfg
386
- // path below handles both on a single connection safely.
382
+ // ── Native bulk-insert fast path ──────────────────────────────────────
383
+ // The Rust bulkInsertCfg handles delete-before-insert atomically on a
384
+ // single rusqlite connection, so there is no dual-connection WAL conflict.
385
+ const nativeDb = engineOpts?.nativeDb;
386
+ if (allNative && nativeDb?.bulkInsertCfg) {
387
+ const entries: Array<Record<string, unknown>> = [];
388
+ for (const [relPath, symbols] of fileSymbols) {
389
+ const ext = path.extname(relPath).toLowerCase();
390
+ if (!CFG_EXTENSIONS.has(ext)) continue;
391
+
392
+ for (const def of symbols.definitions) {
393
+ if (def.kind !== 'function' && def.kind !== 'method') continue;
394
+ if (!def.line) continue;
395
+
396
+ const nodeId = getFunctionNodeId(db, def.name, relPath, def.line);
397
+ if (!nodeId) continue;
398
+
399
+ const cfg = def.cfg as { blocks?: CfgBuildBlock[]; edges?: CfgBuildEdge[] } | undefined;
400
+ if (!cfg?.blocks?.length) continue;
401
+
402
+ entries.push({
403
+ nodeId,
404
+ blocks: cfg.blocks.map((b) => ({
405
+ index: b.index,
406
+ blockType: b.type,
407
+ startLine: b.startLine ?? undefined,
408
+ endLine: b.endLine ?? undefined,
409
+ label: b.label ?? undefined,
410
+ })),
411
+ edges: (cfg.edges || []).map((e) => ({
412
+ sourceIndex: e.sourceIndex,
413
+ targetIndex: e.targetIndex,
414
+ kind: e.kind,
415
+ })),
416
+ });
417
+ }
418
+ }
419
+
420
+ if (entries.length > 0) {
421
+ let inserted = 0;
422
+ try {
423
+ engineOpts?.suspendJsDb?.();
424
+ inserted = nativeDb.bulkInsertCfg(entries);
425
+ } finally {
426
+ engineOpts?.resumeJsDb?.();
427
+ }
428
+ info(`CFG (native bulk): ${inserted} functions analyzed`);
429
+ }
430
+ return;
431
+ }
387
432
 
388
433
  const extToLang = buildExtToLangMap();
389
434
  let parsers: unknown = null;
@@ -545,6 +545,10 @@ function collectNativeBulkRows(
545
545
  const rows: Array<Record<string, unknown>> = [];
546
546
 
547
547
  for (const [relPath, symbols] of fileSymbols) {
548
+ const ext = path.extname(relPath).toLowerCase();
549
+ const langId = symbols._langId || '';
550
+ const langSupported = COMPLEXITY_EXTENSIONS.has(ext) || COMPLEXITY_RULES.has(langId);
551
+
548
552
  for (const def of symbols.definitions) {
549
553
  if (def.kind !== 'function' && def.kind !== 'method') continue;
550
554
  if (!def.line) continue;
@@ -554,6 +558,9 @@ function collectNativeBulkRows(
554
558
  // of the native bulk-insert path for every TypeScript codebase (#846).
555
559
  if (!def.complexity) {
556
560
  if (def.name.includes('.') || !def.endLine || def.endLine <= def.line) continue;
561
+ // Languages without complexity rules will never have data — skip them
562
+ // rather than bailing out of the entire native bulk path.
563
+ if (!langSupported) continue;
557
564
  return null; // genuine function body missing complexity — needs JS fallback
558
565
  }
559
566
  const nodeId = getFunctionNodeId(db, def.name, relPath, def.line);
@@ -18,11 +18,11 @@ interface UpdateCache {
18
18
 
19
19
  /**
20
20
  * Minimal semver comparison. Returns -1, 0, or 1.
21
- * Only handles numeric x.y.z (no pre-release tags).
21
+ * Strips pre-release suffixes (e.g. "3.9.3-dev.6" → "3.9.3") before comparing.
22
22
  */
23
23
  export function semverCompare(a: string, b: string): -1 | 0 | 1 {
24
- const pa = a.split('.').map(Number);
25
- const pb = b.split('.').map(Number);
24
+ const pa = a.replace(/-.*$/, '').split('.').map(Number);
25
+ const pb = b.replace(/-.*$/, '').split('.').map(Number);
26
26
  for (let i = 0; i < 3; i++) {
27
27
  const na = pa[i] || 0;
28
28
  const nb = pb[i] || 0;
package/src/types.ts CHANGED
@@ -1874,6 +1874,7 @@ export type StmtCache<TRow = unknown> = WeakMap<BetterSqlite3Database, SqliteSta
1874
1874
  export interface NativeAddon {
1875
1875
  parseFile(filePath: string, source: string, dataflow: boolean, ast: boolean): unknown;
1876
1876
  parseFiles(files: string[], rootDir: string, dataflow: boolean, ast: boolean): unknown[];
1877
+ parseFilesFull?(files: string[], rootDir: string): unknown[];
1877
1878
  resolveImport(fromFile: string, importSource: string, rootDir: string, aliases: unknown): string;
1878
1879
  resolveImports(
1879
1880
  items: Array<{ fromFile: string; importSource: string }>,