@optave/codegraph 3.1.2 → 3.1.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.
package/README.md CHANGED
@@ -562,14 +562,14 @@ Self-measured on every release via CI ([build benchmarks](generated/benchmarks/B
562
562
 
563
563
  | Metric | Latest |
564
564
  |---|---|
565
- | Build speed (native) | **6.1 ms/file** |
566
- | Build speed (WASM) | **16.5 ms/file** |
567
- | Query time | **3ms** |
568
- | No-op rebuild (native) | **5ms** |
569
- | 1-file rebuild (native) | **332ms** |
565
+ | Build speed (native) | **5.2 ms/file** |
566
+ | Build speed (WASM) | **15 ms/file** |
567
+ | Query time | **4ms** |
568
+ | No-op rebuild (native) | **6ms** |
569
+ | 1-file rebuild (native) | **296ms** |
570
570
  | Query: fn-deps | **0.8ms** |
571
571
  | Query: path | **0.8ms** |
572
- | ~50,000 files (est.) | **~305.0s build** |
572
+ | ~50,000 files (est.) | **~260.0s build** |
573
573
 
574
574
  Metrics are normalized per file for cross-version comparability. Times above are for a full initial build — incremental rebuilds only re-parse changed files.
575
575
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@optave/codegraph",
3
- "version": "3.1.2",
3
+ "version": "3.1.3",
4
4
  "description": "Local code graph CLI — parse codebases with tree-sitter, build dependency graphs, query them",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -71,12 +71,12 @@
71
71
  },
72
72
  "optionalDependencies": {
73
73
  "@modelcontextprotocol/sdk": "^1.0.0",
74
- "@optave/codegraph-darwin-arm64": "3.1.2",
75
- "@optave/codegraph-darwin-x64": "3.1.2",
76
- "@optave/codegraph-linux-arm64-gnu": "3.1.2",
77
- "@optave/codegraph-linux-x64-gnu": "3.1.2",
78
- "@optave/codegraph-linux-x64-musl": "3.1.2",
79
- "@optave/codegraph-win32-x64-msvc": "3.1.2"
74
+ "@optave/codegraph-darwin-arm64": "3.1.3",
75
+ "@optave/codegraph-darwin-x64": "3.1.3",
76
+ "@optave/codegraph-linux-arm64-gnu": "3.1.3",
77
+ "@optave/codegraph-linux-x64-gnu": "3.1.3",
78
+ "@optave/codegraph-linux-x64-musl": "3.1.3",
79
+ "@optave/codegraph-win32-x64-msvc": "3.1.3"
80
80
  },
81
81
  "devDependencies": {
82
82
  "@biomejs/biome": "^2.4.4",
package/src/builder.js CHANGED
@@ -364,6 +364,7 @@ export function purgeFilesFromGraph(db, files, options = {}) {
364
364
  }
365
365
 
366
366
  export async function buildGraph(rootDir, opts = {}) {
367
+ const _t_buildStart = performance.now();
367
368
  rootDir = path.resolve(rootDir);
368
369
  const dbPath = path.join(rootDir, '.codegraph', 'graph.db');
369
370
  const db = openDb(dbPath);
@@ -668,6 +669,7 @@ export async function buildGraph(rootDir, opts = {}) {
668
669
 
669
670
  // ── Phase timing ────────────────────────────────────────────────────
670
671
  const _t = {};
672
+ _t.setupMs = performance.now() - _t_buildStart;
671
673
 
672
674
  // ── Unified parse via parseFilesAuto ───────────────────────────────
673
675
  const filePaths = filesToParse.map((item) => item.file);
@@ -1350,8 +1352,15 @@ export async function buildGraph(rootDir, opts = {}) {
1350
1352
  }
1351
1353
  }
1352
1354
 
1353
- // Release any remaining cached WASM trees for GC
1355
+ _t.finalize0 = performance.now();
1356
+
1357
+ // Release any remaining cached WASM trees — call .delete() to free WASM memory
1354
1358
  for (const [, symbols] of allSymbols) {
1359
+ if (symbols._tree && typeof symbols._tree.delete === 'function') {
1360
+ try {
1361
+ symbols._tree.delete();
1362
+ } catch {}
1363
+ }
1355
1364
  symbols._tree = null;
1356
1365
  symbols._langId = null;
1357
1366
  }
@@ -1456,8 +1465,11 @@ export async function buildGraph(rootDir, opts = {}) {
1456
1465
  }
1457
1466
  }
1458
1467
 
1468
+ _t.finalizeMs = performance.now() - _t.finalize0;
1469
+
1459
1470
  return {
1460
1471
  phases: {
1472
+ setupMs: +_t.setupMs.toFixed(1),
1461
1473
  parseMs: +_t.parseMs.toFixed(1),
1462
1474
  insertMs: +_t.insertMs.toFixed(1),
1463
1475
  resolveMs: +_t.resolveMs.toFixed(1),
@@ -1468,6 +1480,7 @@ export async function buildGraph(rootDir, opts = {}) {
1468
1480
  complexityMs: +_t.complexityMs.toFixed(1),
1469
1481
  ...(_t.cfgMs != null && { cfgMs: +_t.cfgMs.toFixed(1) }),
1470
1482
  ...(_t.dataflowMs != null && { dataflowMs: +_t.dataflowMs.toFixed(1) }),
1483
+ finalizeMs: +_t.finalizeMs.toFixed(1),
1471
1484
  },
1472
1485
  };
1473
1486
  }
package/src/cli.js CHANGED
@@ -1390,7 +1390,7 @@ program
1390
1390
  .command('info')
1391
1391
  .description('Show codegraph engine info and diagnostics')
1392
1392
  .action(async () => {
1393
- const { isNativeAvailable, loadNative } = await import('./native.js');
1393
+ const { getNativePackageVersion, isNativeAvailable, loadNative } = await import('./native.js');
1394
1394
  const { getActiveEngine } = await import('./parser.js');
1395
1395
 
1396
1396
  const engine = program.opts().engine;
@@ -1405,9 +1405,17 @@ program
1405
1405
  console.log(` Native engine : ${nativeAvailable ? 'available' : 'unavailable'}`);
1406
1406
  if (nativeAvailable) {
1407
1407
  const native = loadNative();
1408
- const nativeVersion =
1408
+ const binaryVersion =
1409
1409
  typeof native.engineVersion === 'function' ? native.engineVersion() : 'unknown';
1410
- console.log(` Native version: ${nativeVersion}`);
1410
+ const pkgVersion = getNativePackageVersion();
1411
+ const knownBinaryVersion = binaryVersion !== 'unknown' ? binaryVersion : null;
1412
+ if (pkgVersion && knownBinaryVersion && pkgVersion !== knownBinaryVersion) {
1413
+ console.log(
1414
+ ` Native version: ${pkgVersion} (binary reports ${knownBinaryVersion} — stale)`,
1415
+ );
1416
+ } else {
1417
+ console.log(` Native version: ${pkgVersion ?? binaryVersion}`);
1418
+ }
1411
1419
  }
1412
1420
  console.log(` Engine flag : --engine ${engine}`);
1413
1421
  console.log(` Active engine : ${activeName}${activeVersion ? ` (v${activeVersion})` : ''}`);
package/src/complexity.js CHANGED
@@ -454,7 +454,7 @@ export async function buildComplexityMetrics(db, fileSymbols, rootDir, _engineOp
454
454
  // Fallback: compute from AST tree
455
455
  if (!tree || !rules) continue;
456
456
 
457
- const funcNode = findFunctionNode(tree.rootNode, def.line, def.endLine, rules);
457
+ const funcNode = _findFunctionNode(tree.rootNode, def.line, def.endLine, rules);
458
458
  if (!funcNode) continue;
459
459
 
460
460
  // Single-pass: complexity + Halstead + LOC + MI in one DFS walk
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Resolve a cached prepared statement, compiling on first use per db.
3
+ * Each `cache` WeakMap must always be called with the same `sql` —
4
+ * the sql argument is only used on the first compile; subsequent calls
5
+ * return the cached statement regardless of the sql passed.
6
+ *
7
+ * @param {WeakMap} cache - WeakMap keyed by db instance
8
+ * @param {object} db - better-sqlite3 database instance
9
+ * @param {string} sql - SQL to compile on first use
10
+ * @returns {object} prepared statement
11
+ */
12
+ export function cachedStmt(cache, db, sql) {
13
+ let stmt = cache.get(db);
14
+ if (!stmt) {
15
+ stmt = db.prepare(sql);
16
+ cache.set(db, stmt);
17
+ }
18
+ return stmt;
19
+ }
@@ -1,5 +1,6 @@
1
+ import { cachedStmt } from './cached-stmt.js';
2
+
1
3
  // ─── Statement caches (one prepared statement per db instance) ────────────
2
- // WeakMap keys on the db object so statements are GC'd when the db closes.
3
4
  const _getCfgBlocksStmt = new WeakMap();
4
5
  const _getCfgEdgesStmt = new WeakMap();
5
6
  const _deleteCfgEdgesStmt = new WeakMap();
@@ -26,16 +27,13 @@ export function hasCfgTables(db) {
26
27
  * @returns {object[]}
27
28
  */
28
29
  export function getCfgBlocks(db, functionNodeId) {
29
- let stmt = _getCfgBlocksStmt.get(db);
30
- if (!stmt) {
31
- stmt = db.prepare(
32
- `SELECT id, block_index, block_type, start_line, end_line, label
33
- FROM cfg_blocks WHERE function_node_id = ?
34
- ORDER BY block_index`,
35
- );
36
- _getCfgBlocksStmt.set(db, stmt);
37
- }
38
- return stmt.all(functionNodeId);
30
+ return cachedStmt(
31
+ _getCfgBlocksStmt,
32
+ db,
33
+ `SELECT id, block_index, block_type, start_line, end_line, label
34
+ FROM cfg_blocks WHERE function_node_id = ?
35
+ ORDER BY block_index`,
36
+ ).all(functionNodeId);
39
37
  }
40
38
 
41
39
  /**
@@ -45,21 +43,18 @@ export function getCfgBlocks(db, functionNodeId) {
45
43
  * @returns {object[]}
46
44
  */
47
45
  export function getCfgEdges(db, functionNodeId) {
48
- let stmt = _getCfgEdgesStmt.get(db);
49
- if (!stmt) {
50
- stmt = db.prepare(
51
- `SELECT e.kind,
52
- sb.block_index AS source_index, sb.block_type AS source_type,
53
- tb.block_index AS target_index, tb.block_type AS target_type
54
- FROM cfg_edges e
55
- JOIN cfg_blocks sb ON e.source_block_id = sb.id
56
- JOIN cfg_blocks tb ON e.target_block_id = tb.id
57
- WHERE e.function_node_id = ?
58
- ORDER BY sb.block_index, tb.block_index`,
59
- );
60
- _getCfgEdgesStmt.set(db, stmt);
61
- }
62
- return stmt.all(functionNodeId);
46
+ return cachedStmt(
47
+ _getCfgEdgesStmt,
48
+ db,
49
+ `SELECT e.kind,
50
+ sb.block_index AS source_index, sb.block_type AS source_type,
51
+ tb.block_index AS target_index, tb.block_type AS target_type
52
+ FROM cfg_edges e
53
+ JOIN cfg_blocks sb ON e.source_block_id = sb.id
54
+ JOIN cfg_blocks tb ON e.target_block_id = tb.id
55
+ WHERE e.function_node_id = ?
56
+ ORDER BY sb.block_index, tb.block_index`,
57
+ ).all(functionNodeId);
63
58
  }
64
59
 
65
60
  /**
@@ -68,16 +63,10 @@ export function getCfgEdges(db, functionNodeId) {
68
63
  * @param {number} functionNodeId
69
64
  */
70
65
  export function deleteCfgForNode(db, functionNodeId) {
71
- let delEdges = _deleteCfgEdgesStmt.get(db);
72
- if (!delEdges) {
73
- delEdges = db.prepare('DELETE FROM cfg_edges WHERE function_node_id = ?');
74
- _deleteCfgEdgesStmt.set(db, delEdges);
75
- }
76
- let delBlocks = _deleteCfgBlocksStmt.get(db);
77
- if (!delBlocks) {
78
- delBlocks = db.prepare('DELETE FROM cfg_blocks WHERE function_node_id = ?');
79
- _deleteCfgBlocksStmt.set(db, delBlocks);
80
- }
81
- delEdges.run(functionNodeId);
82
- delBlocks.run(functionNodeId);
66
+ cachedStmt(_deleteCfgEdgesStmt, db, 'DELETE FROM cfg_edges WHERE function_node_id = ?').run(
67
+ functionNodeId,
68
+ );
69
+ cachedStmt(_deleteCfgBlocksStmt, db, 'DELETE FROM cfg_blocks WHERE function_node_id = ?').run(
70
+ functionNodeId,
71
+ );
83
72
  }
@@ -1,3 +1,10 @@
1
+ import { cachedStmt } from './cached-stmt.js';
2
+
3
+ // ─── Statement caches (one prepared statement per db instance) ────────────
4
+ const _hasCoChangesStmt = new WeakMap();
5
+ const _getCoChangeMetaStmt = new WeakMap();
6
+ const _upsertCoChangeMetaStmt = new WeakMap();
7
+
1
8
  /**
2
9
  * Check whether the co_changes table has data.
3
10
  * @param {object} db
@@ -5,7 +12,7 @@
5
12
  */
6
13
  export function hasCoChanges(db) {
7
14
  try {
8
- return !!db.prepare('SELECT 1 FROM co_changes LIMIT 1').get();
15
+ return !!cachedStmt(_hasCoChangesStmt, db, 'SELECT 1 FROM co_changes LIMIT 1').get();
9
16
  } catch {
10
17
  return false;
11
18
  }
@@ -19,7 +26,11 @@ export function hasCoChanges(db) {
19
26
  export function getCoChangeMeta(db) {
20
27
  const meta = {};
21
28
  try {
22
- for (const row of db.prepare('SELECT key, value FROM co_change_meta').all()) {
29
+ for (const row of cachedStmt(
30
+ _getCoChangeMetaStmt,
31
+ db,
32
+ 'SELECT key, value FROM co_change_meta',
33
+ ).all()) {
23
34
  meta[row.key] = row.value;
24
35
  }
25
36
  } catch {
@@ -35,7 +46,9 @@ export function getCoChangeMeta(db) {
35
46
  * @param {string} value
36
47
  */
37
48
  export function upsertCoChangeMeta(db, key, value) {
38
- db.prepare(
49
+ cachedStmt(
50
+ _upsertCoChangeMetaStmt,
51
+ db,
39
52
  'INSERT INTO co_change_meta (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value',
40
53
  ).run(key, value);
41
54
  }
@@ -1,3 +1,8 @@
1
+ import { cachedStmt } from './cached-stmt.js';
2
+
3
+ // ─── Statement caches (one prepared statement per db instance) ────────────
4
+ const _getComplexityForNodeStmt = new WeakMap();
5
+
1
6
  /**
2
7
  * Get complexity metrics for a node.
3
8
  * Used by contextData and explainFunctionImpl in queries.js.
@@ -6,10 +11,10 @@
6
11
  * @returns {{ cognitive: number, cyclomatic: number, max_nesting: number, maintainability_index: number, halstead_volume: number }|undefined}
7
12
  */
8
13
  export function getComplexityForNode(db, nodeId) {
9
- return db
10
- .prepare(
11
- `SELECT cognitive, cyclomatic, max_nesting, maintainability_index, halstead_volume
12
- FROM function_complexity WHERE node_id = ?`,
13
- )
14
- .get(nodeId);
14
+ return cachedStmt(
15
+ _getComplexityForNodeStmt,
16
+ db,
17
+ `SELECT cognitive, cyclomatic, max_nesting, maintainability_index, halstead_volume
18
+ FROM function_complexity WHERE node_id = ?`,
19
+ ).get(nodeId);
15
20
  }
@@ -1,3 +1,8 @@
1
+ import { cachedStmt } from './cached-stmt.js';
2
+
3
+ // ─── Statement caches (one prepared statement per db instance) ────────────
4
+ const _hasDataflowTableStmt = new WeakMap();
5
+
1
6
  /**
2
7
  * Check whether the dataflow table exists and has data.
3
8
  * @param {object} db
@@ -5,7 +10,7 @@
5
10
  */
6
11
  export function hasDataflowTable(db) {
7
12
  try {
8
- return db.prepare('SELECT COUNT(*) AS c FROM dataflow').get().c > 0;
13
+ return cachedStmt(_hasDataflowTableStmt, db, 'SELECT COUNT(*) AS c FROM dataflow').get().c > 0;
9
14
  } catch {
10
15
  return false;
11
16
  }
@@ -1,3 +1,21 @@
1
+ import { cachedStmt } from './cached-stmt.js';
2
+
3
+ // ─── Prepared-statement caches (one per db instance) ────────────────────
4
+ const _findCalleesStmt = new WeakMap();
5
+ const _findCallersStmt = new WeakMap();
6
+ const _findDistinctCallersStmt = new WeakMap();
7
+ const _findAllOutgoingStmt = new WeakMap();
8
+ const _findAllIncomingStmt = new WeakMap();
9
+ const _findCalleeNamesStmt = new WeakMap();
10
+ const _findCallerNamesStmt = new WeakMap();
11
+ const _findImportTargetsStmt = new WeakMap();
12
+ const _findImportSourcesStmt = new WeakMap();
13
+ const _findImportDependentsStmt = new WeakMap();
14
+ const _findCrossFileCallTargetsStmt = new WeakMap();
15
+ const _countCrossFileCallersStmt = new WeakMap();
16
+ const _getClassAncestorsStmt = new WeakMap();
17
+ const _findIntraFileCallEdgesStmt = new WeakMap();
18
+
1
19
  // ─── Call-edge queries ──────────────────────────────────────────────────
2
20
 
3
21
  /**
@@ -8,13 +26,13 @@
8
26
  * @returns {{ id: number, name: string, kind: string, file: string, line: number, end_line: number|null }[]}
9
27
  */
10
28
  export function findCallees(db, nodeId) {
11
- return db
12
- .prepare(
13
- `SELECT DISTINCT n.id, n.name, n.kind, n.file, n.line, n.end_line
14
- FROM edges e JOIN nodes n ON e.target_id = n.id
15
- WHERE e.source_id = ? AND e.kind = 'calls'`,
16
- )
17
- .all(nodeId);
29
+ return cachedStmt(
30
+ _findCalleesStmt,
31
+ db,
32
+ `SELECT DISTINCT n.id, n.name, n.kind, n.file, n.line, n.end_line
33
+ FROM edges e JOIN nodes n ON e.target_id = n.id
34
+ WHERE e.source_id = ? AND e.kind = 'calls'`,
35
+ ).all(nodeId);
18
36
  }
19
37
 
20
38
  /**
@@ -24,13 +42,13 @@ export function findCallees(db, nodeId) {
24
42
  * @returns {{ id: number, name: string, kind: string, file: string, line: number }[]}
25
43
  */
26
44
  export function findCallers(db, nodeId) {
27
- return db
28
- .prepare(
29
- `SELECT n.id, n.name, n.kind, n.file, n.line
30
- FROM edges e JOIN nodes n ON e.source_id = n.id
31
- WHERE e.target_id = ? AND e.kind = 'calls'`,
32
- )
33
- .all(nodeId);
45
+ return cachedStmt(
46
+ _findCallersStmt,
47
+ db,
48
+ `SELECT n.id, n.name, n.kind, n.file, n.line
49
+ FROM edges e JOIN nodes n ON e.source_id = n.id
50
+ WHERE e.target_id = ? AND e.kind = 'calls'`,
51
+ ).all(nodeId);
34
52
  }
35
53
 
36
54
  /**
@@ -40,13 +58,13 @@ export function findCallers(db, nodeId) {
40
58
  * @returns {{ id: number, name: string, kind: string, file: string, line: number }[]}
41
59
  */
42
60
  export function findDistinctCallers(db, nodeId) {
43
- return db
44
- .prepare(
45
- `SELECT DISTINCT n.id, n.name, n.kind, n.file, n.line
46
- FROM edges e JOIN nodes n ON e.source_id = n.id
47
- WHERE e.target_id = ? AND e.kind = 'calls'`,
48
- )
49
- .all(nodeId);
61
+ return cachedStmt(
62
+ _findDistinctCallersStmt,
63
+ db,
64
+ `SELECT DISTINCT n.id, n.name, n.kind, n.file, n.line
65
+ FROM edges e JOIN nodes n ON e.source_id = n.id
66
+ WHERE e.target_id = ? AND e.kind = 'calls'`,
67
+ ).all(nodeId);
50
68
  }
51
69
 
52
70
  // ─── All-edge queries (no kind filter) ─────────────────────────────────
@@ -58,13 +76,13 @@ export function findDistinctCallers(db, nodeId) {
58
76
  * @returns {{ name: string, kind: string, file: string, line: number, edge_kind: string }[]}
59
77
  */
60
78
  export function findAllOutgoingEdges(db, nodeId) {
61
- return db
62
- .prepare(
63
- `SELECT n.name, n.kind, n.file, n.line, e.kind AS edge_kind
64
- FROM edges e JOIN nodes n ON e.target_id = n.id
65
- WHERE e.source_id = ?`,
66
- )
67
- .all(nodeId);
79
+ return cachedStmt(
80
+ _findAllOutgoingStmt,
81
+ db,
82
+ `SELECT n.name, n.kind, n.file, n.line, e.kind AS edge_kind
83
+ FROM edges e JOIN nodes n ON e.target_id = n.id
84
+ WHERE e.source_id = ?`,
85
+ ).all(nodeId);
68
86
  }
69
87
 
70
88
  /**
@@ -74,13 +92,13 @@ export function findAllOutgoingEdges(db, nodeId) {
74
92
  * @returns {{ name: string, kind: string, file: string, line: number, edge_kind: string }[]}
75
93
  */
76
94
  export function findAllIncomingEdges(db, nodeId) {
77
- return db
78
- .prepare(
79
- `SELECT n.name, n.kind, n.file, n.line, e.kind AS edge_kind
80
- FROM edges e JOIN nodes n ON e.source_id = n.id
81
- WHERE e.target_id = ?`,
82
- )
83
- .all(nodeId);
95
+ return cachedStmt(
96
+ _findAllIncomingStmt,
97
+ db,
98
+ `SELECT n.name, n.kind, n.file, n.line, e.kind AS edge_kind
99
+ FROM edges e JOIN nodes n ON e.source_id = n.id
100
+ WHERE e.target_id = ?`,
101
+ ).all(nodeId);
84
102
  }
85
103
 
86
104
  // ─── Name-only callee/caller lookups (for embedder) ────────────────────
@@ -92,13 +110,14 @@ export function findAllIncomingEdges(db, nodeId) {
92
110
  * @returns {string[]}
93
111
  */
94
112
  export function findCalleeNames(db, nodeId) {
95
- return db
96
- .prepare(
97
- `SELECT DISTINCT n.name
98
- FROM edges e JOIN nodes n ON e.target_id = n.id
99
- WHERE e.source_id = ? AND e.kind = 'calls'
100
- ORDER BY n.name`,
101
- )
113
+ return cachedStmt(
114
+ _findCalleeNamesStmt,
115
+ db,
116
+ `SELECT DISTINCT n.name
117
+ FROM edges e JOIN nodes n ON e.target_id = n.id
118
+ WHERE e.source_id = ? AND e.kind = 'calls'
119
+ ORDER BY n.name`,
120
+ )
102
121
  .all(nodeId)
103
122
  .map((r) => r.name);
104
123
  }
@@ -110,13 +129,14 @@ export function findCalleeNames(db, nodeId) {
110
129
  * @returns {string[]}
111
130
  */
112
131
  export function findCallerNames(db, nodeId) {
113
- return db
114
- .prepare(
115
- `SELECT DISTINCT n.name
116
- FROM edges e JOIN nodes n ON e.source_id = n.id
117
- WHERE e.target_id = ? AND e.kind = 'calls'
118
- ORDER BY n.name`,
119
- )
132
+ return cachedStmt(
133
+ _findCallerNamesStmt,
134
+ db,
135
+ `SELECT DISTINCT n.name
136
+ FROM edges e JOIN nodes n ON e.source_id = n.id
137
+ WHERE e.target_id = ? AND e.kind = 'calls'
138
+ ORDER BY n.name`,
139
+ )
120
140
  .all(nodeId)
121
141
  .map((r) => r.name);
122
142
  }
@@ -130,13 +150,13 @@ export function findCallerNames(db, nodeId) {
130
150
  * @returns {{ file: string, edge_kind: string }[]}
131
151
  */
132
152
  export function findImportTargets(db, nodeId) {
133
- return db
134
- .prepare(
135
- `SELECT n.file, e.kind AS edge_kind
136
- FROM edges e JOIN nodes n ON e.target_id = n.id
137
- WHERE e.source_id = ? AND e.kind IN ('imports', 'imports-type')`,
138
- )
139
- .all(nodeId);
153
+ return cachedStmt(
154
+ _findImportTargetsStmt,
155
+ db,
156
+ `SELECT n.file, e.kind AS edge_kind
157
+ FROM edges e JOIN nodes n ON e.target_id = n.id
158
+ WHERE e.source_id = ? AND e.kind IN ('imports', 'imports-type')`,
159
+ ).all(nodeId);
140
160
  }
141
161
 
142
162
  /**
@@ -146,13 +166,13 @@ export function findImportTargets(db, nodeId) {
146
166
  * @returns {{ file: string, edge_kind: string }[]}
147
167
  */
148
168
  export function findImportSources(db, nodeId) {
149
- return db
150
- .prepare(
151
- `SELECT n.file, e.kind AS edge_kind
152
- FROM edges e JOIN nodes n ON e.source_id = n.id
153
- WHERE e.target_id = ? AND e.kind IN ('imports', 'imports-type')`,
154
- )
155
- .all(nodeId);
169
+ return cachedStmt(
170
+ _findImportSourcesStmt,
171
+ db,
172
+ `SELECT n.file, e.kind AS edge_kind
173
+ FROM edges e JOIN nodes n ON e.source_id = n.id
174
+ WHERE e.target_id = ? AND e.kind IN ('imports', 'imports-type')`,
175
+ ).all(nodeId);
156
176
  }
157
177
 
158
178
  /**
@@ -163,12 +183,12 @@ export function findImportSources(db, nodeId) {
163
183
  * @returns {object[]}
164
184
  */
165
185
  export function findImportDependents(db, nodeId) {
166
- return db
167
- .prepare(
168
- `SELECT n.* FROM edges e JOIN nodes n ON e.source_id = n.id
169
- WHERE e.target_id = ? AND e.kind IN ('imports', 'imports-type')`,
170
- )
171
- .all(nodeId);
186
+ return cachedStmt(
187
+ _findImportDependentsStmt,
188
+ db,
189
+ `SELECT n.* FROM edges e JOIN nodes n ON e.source_id = n.id
190
+ WHERE e.target_id = ? AND e.kind IN ('imports', 'imports-type')`,
191
+ ).all(nodeId);
172
192
  }
173
193
 
174
194
  // ─── Cross-file and hierarchy queries ──────────────────────────────────
@@ -182,13 +202,14 @@ export function findImportDependents(db, nodeId) {
182
202
  */
183
203
  export function findCrossFileCallTargets(db, file) {
184
204
  return new Set(
185
- db
186
- .prepare(
187
- `SELECT DISTINCT e.target_id FROM edges e
188
- JOIN nodes caller ON e.source_id = caller.id
189
- JOIN nodes target ON e.target_id = target.id
190
- WHERE target.file = ? AND caller.file != ? AND e.kind = 'calls'`,
191
- )
205
+ cachedStmt(
206
+ _findCrossFileCallTargetsStmt,
207
+ db,
208
+ `SELECT DISTINCT e.target_id FROM edges e
209
+ JOIN nodes caller ON e.source_id = caller.id
210
+ JOIN nodes target ON e.target_id = target.id
211
+ WHERE target.file = ? AND caller.file != ? AND e.kind = 'calls'`,
212
+ )
192
213
  .all(file, file)
193
214
  .map((r) => r.target_id),
194
215
  );
@@ -203,12 +224,12 @@ export function findCrossFileCallTargets(db, file) {
203
224
  * @returns {number}
204
225
  */
205
226
  export function countCrossFileCallers(db, nodeId, file) {
206
- return db
207
- .prepare(
208
- `SELECT COUNT(*) AS cnt FROM edges e JOIN nodes n ON e.source_id = n.id
209
- WHERE e.target_id = ? AND e.kind = 'calls' AND n.file != ?`,
210
- )
211
- .get(nodeId, file).cnt;
227
+ return cachedStmt(
228
+ _countCrossFileCallersStmt,
229
+ db,
230
+ `SELECT COUNT(*) AS cnt FROM edges e JOIN nodes n ON e.source_id = n.id
231
+ WHERE e.target_id = ? AND e.kind = 'calls' AND n.file != ?`,
232
+ ).get(nodeId, file).cnt;
212
233
  }
213
234
 
214
235
  /**
@@ -220,14 +241,15 @@ export function countCrossFileCallers(db, nodeId, file) {
220
241
  export function getClassHierarchy(db, classNodeId) {
221
242
  const ancestors = new Set();
222
243
  const queue = [classNodeId];
244
+ const stmt = cachedStmt(
245
+ _getClassAncestorsStmt,
246
+ db,
247
+ `SELECT n.id, n.name FROM edges e JOIN nodes n ON e.target_id = n.id
248
+ WHERE e.source_id = ? AND e.kind = 'extends'`,
249
+ );
223
250
  while (queue.length > 0) {
224
251
  const current = queue.shift();
225
- const parents = db
226
- .prepare(
227
- `SELECT n.id, n.name FROM edges e JOIN nodes n ON e.target_id = n.id
228
- WHERE e.source_id = ? AND e.kind = 'extends'`,
229
- )
230
- .all(current);
252
+ const parents = stmt.all(current);
231
253
  for (const p of parents) {
232
254
  if (!ancestors.has(p.id)) {
233
255
  ancestors.add(p.id);
@@ -246,14 +268,14 @@ export function getClassHierarchy(db, classNodeId) {
246
268
  * @returns {{ caller_name: string, callee_name: string }[]}
247
269
  */
248
270
  export function findIntraFileCallEdges(db, file) {
249
- return db
250
- .prepare(
251
- `SELECT caller.name AS caller_name, callee.name AS callee_name
252
- FROM edges e
253
- JOIN nodes caller ON e.source_id = caller.id
254
- JOIN nodes callee ON e.target_id = callee.id
255
- WHERE caller.file = ? AND callee.file = ? AND e.kind = 'calls'
256
- ORDER BY caller.line`,
257
- )
258
- .all(file, file);
271
+ return cachedStmt(
272
+ _findIntraFileCallEdgesStmt,
273
+ db,
274
+ `SELECT caller.name AS caller_name, callee.name AS callee_name
275
+ FROM edges e
276
+ JOIN nodes caller ON e.source_id = caller.id
277
+ JOIN nodes callee ON e.target_id = callee.id
278
+ WHERE caller.file = ? AND callee.file = ? AND e.kind = 'calls'
279
+ ORDER BY caller.line`,
280
+ ).all(file, file);
259
281
  }
@@ -1,3 +1,10 @@
1
+ import { cachedStmt } from './cached-stmt.js';
2
+
3
+ // ─── Statement caches (one prepared statement per db instance) ────────────
4
+ const _hasEmbeddingsStmt = new WeakMap();
5
+ const _getEmbeddingCountStmt = new WeakMap();
6
+ const _getEmbeddingMetaStmt = new WeakMap();
7
+
1
8
  /**
2
9
  * Check whether the embeddings table has data.
3
10
  * @param {object} db
@@ -5,7 +12,7 @@
5
12
  */
6
13
  export function hasEmbeddings(db) {
7
14
  try {
8
- return !!db.prepare('SELECT 1 FROM embeddings LIMIT 1').get();
15
+ return !!cachedStmt(_hasEmbeddingsStmt, db, 'SELECT 1 FROM embeddings LIMIT 1').get();
9
16
  } catch {
10
17
  return false;
11
18
  }
@@ -18,7 +25,7 @@ export function hasEmbeddings(db) {
18
25
  */
19
26
  export function getEmbeddingCount(db) {
20
27
  try {
21
- return db.prepare('SELECT COUNT(*) AS c FROM embeddings').get().c;
28
+ return cachedStmt(_getEmbeddingCountStmt, db, 'SELECT COUNT(*) AS c FROM embeddings').get().c;
22
29
  } catch {
23
30
  return 0;
24
31
  }
@@ -32,7 +39,11 @@ export function getEmbeddingCount(db) {
32
39
  */
33
40
  export function getEmbeddingMeta(db, key) {
34
41
  try {
35
- const row = db.prepare('SELECT value FROM embedding_meta WHERE key = ?').get(key);
42
+ const row = cachedStmt(
43
+ _getEmbeddingMetaStmt,
44
+ db,
45
+ 'SELECT value FROM embedding_meta WHERE key = ?',
46
+ ).get(key);
36
47
  return row?.value;
37
48
  } catch {
38
49
  return undefined;
@@ -1,12 +1,22 @@
1
+ import { cachedStmt } from './cached-stmt.js';
2
+
3
+ // ─── Statement caches (one prepared statement per db instance) ────────────
4
+ const _getCallableNodesStmt = new WeakMap();
5
+ const _getCallEdgesStmt = new WeakMap();
6
+ const _getFileNodesAllStmt = new WeakMap();
7
+ const _getImportEdgesStmt = new WeakMap();
8
+
1
9
  /**
2
10
  * Get callable nodes (function/method/class) for community detection.
3
11
  * @param {object} db
4
12
  * @returns {{ id: number, name: string, kind: string, file: string }[]}
5
13
  */
6
14
  export function getCallableNodes(db) {
7
- return db
8
- .prepare("SELECT id, name, kind, file FROM nodes WHERE kind IN ('function','method','class')")
9
- .all();
15
+ return cachedStmt(
16
+ _getCallableNodesStmt,
17
+ db,
18
+ "SELECT id, name, kind, file FROM nodes WHERE kind IN ('function','method','class')",
19
+ ).all();
10
20
  }
11
21
 
12
22
  /**
@@ -15,7 +25,11 @@ export function getCallableNodes(db) {
15
25
  * @returns {{ source_id: number, target_id: number }[]}
16
26
  */
17
27
  export function getCallEdges(db) {
18
- return db.prepare("SELECT source_id, target_id FROM edges WHERE kind = 'calls'").all();
28
+ return cachedStmt(
29
+ _getCallEdgesStmt,
30
+ db,
31
+ "SELECT source_id, target_id FROM edges WHERE kind = 'calls'",
32
+ ).all();
19
33
  }
20
34
 
21
35
  /**
@@ -24,7 +38,11 @@ export function getCallEdges(db) {
24
38
  * @returns {{ id: number, name: string, file: string }[]}
25
39
  */
26
40
  export function getFileNodesAll(db) {
27
- return db.prepare("SELECT id, name, file FROM nodes WHERE kind = 'file'").all();
41
+ return cachedStmt(
42
+ _getFileNodesAllStmt,
43
+ db,
44
+ "SELECT id, name, file FROM nodes WHERE kind = 'file'",
45
+ ).all();
28
46
  }
29
47
 
30
48
  /**
@@ -33,7 +51,9 @@ export function getFileNodesAll(db) {
33
51
  * @returns {{ source_id: number, target_id: number }[]}
34
52
  */
35
53
  export function getImportEdges(db) {
36
- return db
37
- .prepare("SELECT source_id, target_id FROM edges WHERE kind IN ('imports','imports-type')")
38
- .all();
54
+ return cachedStmt(
55
+ _getImportEdgesStmt,
56
+ db,
57
+ "SELECT source_id, target_id FROM edges WHERE kind IN ('imports','imports-type')",
58
+ ).all();
39
59
  }
@@ -1,6 +1,7 @@
1
1
  // Barrel re-export for repository/ modules.
2
2
 
3
3
  export { purgeFileData, purgeFilesData } from './build-stmts.js';
4
+ export { cachedStmt } from './cached-stmt.js';
4
5
  export { deleteCfgForNode, getCfgBlocks, getCfgEdges, hasCfgTables } from './cfg.js';
5
6
  export { getCoChangeMeta, hasCoChanges, upsertCoChangeMeta } from './cochange.js';
6
7
 
@@ -1,5 +1,6 @@
1
1
  import { EVERY_SYMBOL_KIND, VALID_ROLES } from '../../kinds.js';
2
2
  import { NodeQuery } from '../query-builder.js';
3
+ import { cachedStmt } from './cached-stmt.js';
3
4
 
4
5
  // ─── Query-builder based lookups (moved from src/db/repository.js) ─────
5
6
 
@@ -100,13 +101,26 @@ export function iterateFunctionNodes(db, opts = {}) {
100
101
  return _functionNodeQuery(opts).iterate(db);
101
102
  }
102
103
 
104
+ // ─── Statement caches (one prepared statement per db instance) ────────────
105
+ // WeakMap keys on the db object so statements are GC'd when the db closes.
106
+ const _countNodesStmt = new WeakMap();
107
+ const _countEdgesStmt = new WeakMap();
108
+ const _countFilesStmt = new WeakMap();
109
+ const _findNodeByIdStmt = new WeakMap();
110
+ const _findNodesByFileStmt = new WeakMap();
111
+ const _findFileNodesStmt = new WeakMap();
112
+ const _getNodeIdStmt = new WeakMap();
113
+ const _getFunctionNodeIdStmt = new WeakMap();
114
+ const _bulkNodeIdsByFileStmt = new WeakMap();
115
+ const _findNodeChildrenStmt = new WeakMap();
116
+
103
117
  /**
104
118
  * Count total nodes.
105
119
  * @param {object} db
106
120
  * @returns {number}
107
121
  */
108
122
  export function countNodes(db) {
109
- return db.prepare('SELECT COUNT(*) AS cnt FROM nodes').get().cnt;
123
+ return cachedStmt(_countNodesStmt, db, 'SELECT COUNT(*) AS cnt FROM nodes').get().cnt;
110
124
  }
111
125
 
112
126
  /**
@@ -115,7 +129,7 @@ export function countNodes(db) {
115
129
  * @returns {number}
116
130
  */
117
131
  export function countEdges(db) {
118
- return db.prepare('SELECT COUNT(*) AS cnt FROM edges').get().cnt;
132
+ return cachedStmt(_countEdgesStmt, db, 'SELECT COUNT(*) AS cnt FROM edges').get().cnt;
119
133
  }
120
134
 
121
135
  /**
@@ -124,7 +138,7 @@ export function countEdges(db) {
124
138
  * @returns {number}
125
139
  */
126
140
  export function countFiles(db) {
127
- return db.prepare('SELECT COUNT(DISTINCT file) AS cnt FROM nodes').get().cnt;
141
+ return cachedStmt(_countFilesStmt, db, 'SELECT COUNT(DISTINCT file) AS cnt FROM nodes').get().cnt;
128
142
  }
129
143
 
130
144
  // ─── Shared node lookups ───────────────────────────────────────────────
@@ -136,7 +150,7 @@ export function countFiles(db) {
136
150
  * @returns {object|undefined}
137
151
  */
138
152
  export function findNodeById(db, id) {
139
- return db.prepare('SELECT * FROM nodes WHERE id = ?').get(id);
153
+ return cachedStmt(_findNodeByIdStmt, db, 'SELECT * FROM nodes WHERE id = ?').get(id);
140
154
  }
141
155
 
142
156
  /**
@@ -146,9 +160,11 @@ export function findNodeById(db, id) {
146
160
  * @returns {object[]}
147
161
  */
148
162
  export function findNodesByFile(db, file) {
149
- return db
150
- .prepare("SELECT * FROM nodes WHERE file = ? AND kind != 'file' ORDER BY line")
151
- .all(file);
163
+ return cachedStmt(
164
+ _findNodesByFileStmt,
165
+ db,
166
+ "SELECT * FROM nodes WHERE file = ? AND kind != 'file' ORDER BY line",
167
+ ).all(file);
152
168
  }
153
169
 
154
170
  /**
@@ -158,15 +174,13 @@ export function findNodesByFile(db, file) {
158
174
  * @returns {object[]}
159
175
  */
160
176
  export function findFileNodes(db, fileLike) {
161
- return db.prepare("SELECT * FROM nodes WHERE file LIKE ? AND kind = 'file'").all(fileLike);
177
+ return cachedStmt(
178
+ _findFileNodesStmt,
179
+ db,
180
+ "SELECT * FROM nodes WHERE file LIKE ? AND kind = 'file'",
181
+ ).all(fileLike);
162
182
  }
163
183
 
164
- // ─── Statement caches (one prepared statement per db instance) ────────────
165
- // WeakMap keys on the db object so statements are GC'd when the db closes.
166
- const _getNodeIdStmt = new WeakMap();
167
- const _getFunctionNodeIdStmt = new WeakMap();
168
- const _bulkNodeIdsByFileStmt = new WeakMap();
169
-
170
184
  /**
171
185
  * Look up a node's ID by its unique (name, kind, file, line) tuple.
172
186
  * Shared by builder, watcher, structure, complexity, cfg, engine.
@@ -178,12 +192,11 @@ const _bulkNodeIdsByFileStmt = new WeakMap();
178
192
  * @returns {number|undefined}
179
193
  */
180
194
  export function getNodeId(db, name, kind, file, line) {
181
- let stmt = _getNodeIdStmt.get(db);
182
- if (!stmt) {
183
- stmt = db.prepare('SELECT id FROM nodes WHERE name = ? AND kind = ? AND file = ? AND line = ?');
184
- _getNodeIdStmt.set(db, stmt);
185
- }
186
- return stmt.get(name, kind, file, line)?.id;
195
+ return cachedStmt(
196
+ _getNodeIdStmt,
197
+ db,
198
+ 'SELECT id FROM nodes WHERE name = ? AND kind = ? AND file = ? AND line = ?',
199
+ ).get(name, kind, file, line)?.id;
187
200
  }
188
201
 
189
202
  /**
@@ -196,14 +209,11 @@ export function getNodeId(db, name, kind, file, line) {
196
209
  * @returns {number|undefined}
197
210
  */
198
211
  export function getFunctionNodeId(db, name, file, line) {
199
- let stmt = _getFunctionNodeIdStmt.get(db);
200
- if (!stmt) {
201
- stmt = db.prepare(
202
- "SELECT id FROM nodes WHERE name = ? AND kind IN ('function','method') AND file = ? AND line = ?",
203
- );
204
- _getFunctionNodeIdStmt.set(db, stmt);
205
- }
206
- return stmt.get(name, file, line)?.id;
212
+ return cachedStmt(
213
+ _getFunctionNodeIdStmt,
214
+ db,
215
+ "SELECT id FROM nodes WHERE name = ? AND kind IN ('function','method') AND file = ? AND line = ?",
216
+ ).get(name, file, line)?.id;
207
217
  }
208
218
 
209
219
  /**
@@ -215,12 +225,11 @@ export function getFunctionNodeId(db, name, file, line) {
215
225
  * @returns {{ id: number, name: string, kind: string, line: number }[]}
216
226
  */
217
227
  export function bulkNodeIdsByFile(db, file) {
218
- let stmt = _bulkNodeIdsByFileStmt.get(db);
219
- if (!stmt) {
220
- stmt = db.prepare('SELECT id, name, kind, line FROM nodes WHERE file = ?');
221
- _bulkNodeIdsByFileStmt.set(db, stmt);
222
- }
223
- return stmt.all(file);
228
+ return cachedStmt(
229
+ _bulkNodeIdsByFileStmt,
230
+ db,
231
+ 'SELECT id, name, kind, line FROM nodes WHERE file = ?',
232
+ ).all(file);
224
233
  }
225
234
 
226
235
  /**
@@ -230,7 +239,9 @@ export function bulkNodeIdsByFile(db, file) {
230
239
  * @returns {{ name: string, kind: string, line: number, end_line: number|null }[]}
231
240
  */
232
241
  export function findNodeChildren(db, parentId) {
233
- return db
234
- .prepare('SELECT name, kind, line, end_line FROM nodes WHERE parent_id = ? ORDER BY line')
235
- .all(parentId);
242
+ return cachedStmt(
243
+ _findNodeChildrenStmt,
244
+ db,
245
+ 'SELECT name, kind, line, end_line FROM nodes WHERE parent_id = ? ORDER BY line',
246
+ ).all(parentId);
236
247
  }
package/src/index.js CHANGED
@@ -145,7 +145,13 @@ export { matchOwners, ownersData, ownersForFiles, parseCodeowners } from './owne
145
145
  // Pagination utilities
146
146
  export { MCP_DEFAULTS, MCP_MAX_LIMIT, paginate, paginateResult, printNdjson } from './paginate.js';
147
147
  // Unified parser API
148
- export { getActiveEngine, isWasmAvailable, parseFileAuto, parseFilesAuto } from './parser.js';
148
+ export {
149
+ disposeParsers,
150
+ getActiveEngine,
151
+ isWasmAvailable,
152
+ parseFileAuto,
153
+ parseFilesAuto,
154
+ } from './parser.js';
149
155
  // Query functions (data-returning)
150
156
  export {
151
157
  ALL_SYMBOL_KINDS,
package/src/native.js CHANGED
@@ -11,6 +11,7 @@ import os from 'node:os';
11
11
 
12
12
  let _cached; // undefined = not yet tried, null = failed, object = module
13
13
  let _loadError = null;
14
+ const _require = createRequire(import.meta.url);
14
15
 
15
16
  /**
16
17
  * Detect whether the current Linux environment uses glibc or musl.
@@ -18,7 +19,7 @@ let _loadError = null;
18
19
  */
19
20
  function detectLibc() {
20
21
  try {
21
- const { readdirSync } = require('node:fs');
22
+ const { readdirSync } = _require('node:fs');
22
23
  const files = readdirSync('/lib');
23
24
  if (files.some((f) => f.startsWith('ld-musl-') && f.endsWith('.so.1'))) {
24
25
  return 'musl';
@@ -38,6 +39,17 @@ const PLATFORM_PACKAGES = {
38
39
  'win32-x64': '@optave/codegraph-win32-x64-msvc',
39
40
  };
40
41
 
42
+ /**
43
+ * Resolve the platform-specific npm package name for the native addon.
44
+ * Returns null if the current platform is not supported.
45
+ */
46
+ function resolvePlatformPackage() {
47
+ const platform = os.platform();
48
+ const arch = os.arch();
49
+ const key = platform === 'linux' ? `${platform}-${arch}-${detectLibc()}` : `${platform}-${arch}`;
50
+ return PLATFORM_PACKAGES[key] || null;
51
+ }
52
+
41
53
  /**
42
54
  * Try to load the native napi addon.
43
55
  * Returns the module on success, null on failure.
@@ -45,21 +57,16 @@ const PLATFORM_PACKAGES = {
45
57
  export function loadNative() {
46
58
  if (_cached !== undefined) return _cached;
47
59
 
48
- const require = createRequire(import.meta.url);
49
-
50
- const platform = os.platform();
51
- const arch = os.arch();
52
- const key = platform === 'linux' ? `${platform}-${arch}-${detectLibc()}` : `${platform}-${arch}`;
53
- const pkg = PLATFORM_PACKAGES[key];
60
+ const pkg = resolvePlatformPackage();
54
61
  if (pkg) {
55
62
  try {
56
- _cached = require(pkg);
63
+ _cached = _require(pkg);
57
64
  return _cached;
58
65
  } catch (err) {
59
66
  _loadError = err;
60
67
  }
61
68
  } else {
62
- _loadError = new Error(`Unsupported platform: ${key}`);
69
+ _loadError = new Error(`Unsupported platform: ${os.platform()}-${os.arch()}`);
63
70
  }
64
71
 
65
72
  _cached = null;
@@ -73,6 +80,21 @@ export function isNativeAvailable() {
73
80
  return loadNative() !== null;
74
81
  }
75
82
 
83
+ /**
84
+ * Read the version from the platform-specific npm package.json.
85
+ * Returns null if the package is not installed or has no version.
86
+ */
87
+ export function getNativePackageVersion() {
88
+ const pkg = resolvePlatformPackage();
89
+ if (!pkg) return null;
90
+ try {
91
+ const pkgJson = _require(`${pkg}/package.json`);
92
+ return pkgJson.version || null;
93
+ } catch {
94
+ return null;
95
+ }
96
+ }
97
+
76
98
  /**
77
99
  * Return the native module or throw if not available.
78
100
  */
package/src/parser.js CHANGED
@@ -3,7 +3,7 @@ import path from 'node:path';
3
3
  import { fileURLToPath } from 'node:url';
4
4
  import { Language, Parser, Query } from 'web-tree-sitter';
5
5
  import { warn } from './logger.js';
6
- import { getNative, loadNative } from './native.js';
6
+ import { getNative, getNativePackageVersion, loadNative } from './native.js';
7
7
 
8
8
  // Re-export all extractors for backward compatibility
9
9
  export {
@@ -41,6 +41,9 @@ let _initialized = false;
41
41
  // Memoized parsers — avoids reloading WASM grammars on every createParsers() call
42
42
  let _cachedParsers = null;
43
43
 
44
+ // Cached Language objects — WASM-backed, must be .delete()'d explicitly
45
+ let _cachedLanguages = null;
46
+
44
47
  // Query cache for JS/TS/TSX extractors (populated during createParsers)
45
48
  const _queryCache = new Map();
46
49
 
@@ -77,12 +80,14 @@ export async function createParsers() {
77
80
  }
78
81
 
79
82
  const parsers = new Map();
83
+ const languages = new Map();
80
84
  for (const entry of LANGUAGE_REGISTRY) {
81
85
  try {
82
86
  const lang = await Language.load(grammarPath(entry.grammarFile));
83
87
  const parser = new Parser();
84
88
  parser.setLanguage(lang);
85
89
  parsers.set(entry.id, parser);
90
+ languages.set(entry.id, lang);
86
91
  // Compile and cache tree-sitter Query for JS/TS/TSX extractors
87
92
  if (entry.extractor === extractSymbols && !_queryCache.has(entry.id)) {
88
93
  const isTS = entry.id === 'typescript' || entry.id === 'tsx';
@@ -100,9 +105,47 @@ export async function createParsers() {
100
105
  }
101
106
  }
102
107
  _cachedParsers = parsers;
108
+ _cachedLanguages = languages;
103
109
  return parsers;
104
110
  }
105
111
 
112
+ /**
113
+ * Dispose all cached WASM parsers and queries to free WASM linear memory.
114
+ * Call this between repeated builds in the same process (e.g. benchmarks)
115
+ * to prevent memory accumulation that can cause segfaults.
116
+ */
117
+ export function disposeParsers() {
118
+ if (_cachedParsers) {
119
+ for (const [, parser] of _cachedParsers) {
120
+ if (parser && typeof parser.delete === 'function') {
121
+ try {
122
+ parser.delete();
123
+ } catch {}
124
+ }
125
+ }
126
+ _cachedParsers = null;
127
+ }
128
+ for (const [, query] of _queryCache) {
129
+ if (query && typeof query.delete === 'function') {
130
+ try {
131
+ query.delete();
132
+ } catch {}
133
+ }
134
+ }
135
+ _queryCache.clear();
136
+ if (_cachedLanguages) {
137
+ for (const [, lang] of _cachedLanguages) {
138
+ if (lang && typeof lang.delete === 'function') {
139
+ try {
140
+ lang.delete();
141
+ } catch {}
142
+ }
143
+ }
144
+ _cachedLanguages = null;
145
+ }
146
+ _initialized = false;
147
+ }
148
+
106
149
  export function getParser(parsers, filePath) {
107
150
  const ext = path.extname(filePath);
108
151
  const entry = _extToLang.get(ext);
@@ -214,6 +257,7 @@ function patchNativeResult(r) {
214
257
  if (i.csharpUsing === undefined) i.csharpUsing = i.csharp_using;
215
258
  if (i.rubyRequire === undefined) i.rubyRequire = i.ruby_require;
216
259
  if (i.phpUse === undefined) i.phpUse = i.php_use;
260
+ if (i.dynamicImport === undefined) i.dynamicImport = i.dynamic_import;
217
261
  }
218
262
  }
219
263
 
@@ -429,11 +473,18 @@ export async function parseFilesAuto(filePaths, rootDir, opts = {}) {
429
473
  */
430
474
  export function getActiveEngine(opts = {}) {
431
475
  const { name, native } = resolveEngine(opts);
432
- const version = native
476
+ let version = native
433
477
  ? typeof native.engineVersion === 'function'
434
478
  ? native.engineVersion()
435
479
  : null
436
480
  : null;
481
+ // Prefer platform package.json version over binary-embedded version
482
+ // to handle stale binaries that weren't recompiled during a release
483
+ if (native) {
484
+ try {
485
+ version = getNativePackageVersion() ?? version;
486
+ } catch {}
487
+ }
437
488
  return { name, version };
438
489
  }
439
490