@optave/codegraph 3.1.1 → 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.
Files changed (72) hide show
  1. package/README.md +6 -6
  2. package/package.json +7 -7
  3. package/src/ast-analysis/engine.js +365 -0
  4. package/src/ast-analysis/metrics.js +118 -0
  5. package/src/ast-analysis/visitor-utils.js +176 -0
  6. package/src/ast-analysis/visitor.js +162 -0
  7. package/src/ast-analysis/visitors/ast-store-visitor.js +150 -0
  8. package/src/ast-analysis/visitors/cfg-visitor.js +792 -0
  9. package/src/ast-analysis/visitors/complexity-visitor.js +243 -0
  10. package/src/ast-analysis/visitors/dataflow-visitor.js +358 -0
  11. package/src/ast.js +13 -140
  12. package/src/audit.js +2 -87
  13. package/src/batch.js +0 -25
  14. package/src/boundaries.js +1 -1
  15. package/src/branch-compare.js +1 -96
  16. package/src/builder.js +60 -178
  17. package/src/cfg.js +89 -883
  18. package/src/check.js +1 -84
  19. package/src/cli.js +31 -22
  20. package/src/cochange.js +1 -39
  21. package/src/commands/audit.js +88 -0
  22. package/src/commands/batch.js +26 -0
  23. package/src/commands/branch-compare.js +97 -0
  24. package/src/commands/cfg.js +55 -0
  25. package/src/commands/check.js +82 -0
  26. package/src/commands/cochange.js +37 -0
  27. package/src/commands/communities.js +69 -0
  28. package/src/commands/complexity.js +77 -0
  29. package/src/commands/dataflow.js +110 -0
  30. package/src/commands/flow.js +70 -0
  31. package/src/commands/manifesto.js +77 -0
  32. package/src/commands/owners.js +52 -0
  33. package/src/commands/query.js +21 -0
  34. package/src/commands/sequence.js +33 -0
  35. package/src/commands/structure.js +64 -0
  36. package/src/commands/triage.js +49 -0
  37. package/src/communities.js +12 -83
  38. package/src/complexity.js +43 -357
  39. package/src/cycles.js +1 -1
  40. package/src/dataflow.js +12 -665
  41. package/src/db/repository/build-stmts.js +104 -0
  42. package/src/db/repository/cached-stmt.js +19 -0
  43. package/src/db/repository/cfg.js +72 -0
  44. package/src/db/repository/cochange.js +54 -0
  45. package/src/db/repository/complexity.js +20 -0
  46. package/src/db/repository/dataflow.js +17 -0
  47. package/src/db/repository/edges.js +281 -0
  48. package/src/db/repository/embeddings.js +51 -0
  49. package/src/db/repository/graph-read.js +59 -0
  50. package/src/db/repository/index.js +43 -0
  51. package/src/db/repository/nodes.js +247 -0
  52. package/src/db.js +40 -1
  53. package/src/embedder.js +14 -34
  54. package/src/export.js +1 -1
  55. package/src/extractors/javascript.js +130 -5
  56. package/src/flow.js +2 -70
  57. package/src/index.js +30 -20
  58. package/src/{result-formatter.js → infrastructure/result-formatter.js} +1 -1
  59. package/src/kinds.js +1 -0
  60. package/src/manifesto.js +0 -76
  61. package/src/native.js +31 -9
  62. package/src/owners.js +1 -56
  63. package/src/parser.js +53 -2
  64. package/src/queries-cli.js +1 -1
  65. package/src/queries.js +79 -280
  66. package/src/sequence.js +5 -44
  67. package/src/structure.js +16 -75
  68. package/src/triage.js +1 -54
  69. package/src/viewer.js +1 -1
  70. package/src/watcher.js +7 -4
  71. package/src/db/repository.js +0 -134
  72. /package/src/{test-filter.js → infrastructure/test-filter.js} +0 -0
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Prepare all purge statements once, returning an object of runnable stmts.
3
+ * Optional tables are wrapped in try/catch — if the table doesn't exist,
4
+ * that slot is set to null.
5
+ *
6
+ * @param {object} db - Open read-write database handle
7
+ * @returns {object} prepared statements (some may be null)
8
+ */
9
+ function preparePurgeStmts(db) {
10
+ const tryPrepare = (sql) => {
11
+ try {
12
+ return db.prepare(sql);
13
+ } catch {
14
+ return null;
15
+ }
16
+ };
17
+
18
+ return {
19
+ embeddings: tryPrepare(
20
+ 'DELETE FROM embeddings WHERE node_id IN (SELECT id FROM nodes WHERE file = ?)',
21
+ ),
22
+ cfgEdges: tryPrepare(
23
+ 'DELETE FROM cfg_edges WHERE function_node_id IN (SELECT id FROM nodes WHERE file = ?)',
24
+ ),
25
+ cfgBlocks: tryPrepare(
26
+ 'DELETE FROM cfg_blocks WHERE function_node_id IN (SELECT id FROM nodes WHERE file = ?)',
27
+ ),
28
+ dataflow: tryPrepare(
29
+ 'DELETE FROM dataflow WHERE source_id IN (SELECT id FROM nodes WHERE file = ?) OR target_id IN (SELECT id FROM nodes WHERE file = ?)',
30
+ ),
31
+ complexity: tryPrepare(
32
+ 'DELETE FROM function_complexity WHERE node_id IN (SELECT id FROM nodes WHERE file = ?)',
33
+ ),
34
+ nodeMetrics: tryPrepare(
35
+ 'DELETE FROM node_metrics WHERE node_id IN (SELECT id FROM nodes WHERE file = ?)',
36
+ ),
37
+ astNodes: tryPrepare('DELETE FROM ast_nodes WHERE file = ?'),
38
+ // Core tables — always exist
39
+ edges: db.prepare(
40
+ 'DELETE FROM edges WHERE source_id IN (SELECT id FROM nodes WHERE file = @f) OR target_id IN (SELECT id FROM nodes WHERE file = @f)',
41
+ ),
42
+ nodes: db.prepare('DELETE FROM nodes WHERE file = ?'),
43
+ fileHashes: tryPrepare('DELETE FROM file_hashes WHERE file = ?'),
44
+ };
45
+ }
46
+
47
+ /**
48
+ * Cascade-delete all graph data for a single file across all tables.
49
+ * Order: dependent tables first, then edges, then nodes, then hashes.
50
+ *
51
+ * @param {object} db - Open read-write database handle
52
+ * @param {string} file - Relative file path to purge
53
+ * @param {object} [opts]
54
+ * @param {boolean} [opts.purgeHashes=true] - Also delete file_hashes entry
55
+ */
56
+ export function purgeFileData(db, file, opts = {}) {
57
+ const stmts = preparePurgeStmts(db);
58
+ runPurge(stmts, file, opts);
59
+ }
60
+
61
+ /**
62
+ * Run purge using pre-prepared statements for a single file.
63
+ * @param {object} stmts - Prepared statements from preparePurgeStmts
64
+ * @param {string} file - Relative file path to purge
65
+ * @param {object} [opts]
66
+ * @param {boolean} [opts.purgeHashes=true]
67
+ */
68
+ function runPurge(stmts, file, opts = {}) {
69
+ const { purgeHashes = true } = opts;
70
+
71
+ // Optional tables
72
+ stmts.embeddings?.run(file);
73
+ stmts.cfgEdges?.run(file);
74
+ stmts.cfgBlocks?.run(file);
75
+ stmts.dataflow?.run(file, file);
76
+ stmts.complexity?.run(file);
77
+ stmts.nodeMetrics?.run(file);
78
+ stmts.astNodes?.run(file);
79
+
80
+ // Core tables
81
+ stmts.edges.run({ f: file });
82
+ stmts.nodes.run(file);
83
+
84
+ if (purgeHashes) {
85
+ stmts.fileHashes?.run(file);
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Purge all graph data for multiple files.
91
+ * Prepares statements once and loops over files for efficiency.
92
+ *
93
+ * @param {object} db - Open read-write database handle
94
+ * @param {string[]} files - Relative file paths to purge
95
+ * @param {object} [opts]
96
+ * @param {boolean} [opts.purgeHashes=true]
97
+ */
98
+ export function purgeFilesData(db, files, opts = {}) {
99
+ if (!files || files.length === 0) return;
100
+ const stmts = preparePurgeStmts(db);
101
+ for (const file of files) {
102
+ runPurge(stmts, file, opts);
103
+ }
104
+ }
@@ -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
+ }
@@ -0,0 +1,72 @@
1
+ import { cachedStmt } from './cached-stmt.js';
2
+
3
+ // ─── Statement caches (one prepared statement per db instance) ────────────
4
+ const _getCfgBlocksStmt = new WeakMap();
5
+ const _getCfgEdgesStmt = new WeakMap();
6
+ const _deleteCfgEdgesStmt = new WeakMap();
7
+ const _deleteCfgBlocksStmt = new WeakMap();
8
+
9
+ /**
10
+ * Check whether CFG tables exist.
11
+ * @param {object} db
12
+ * @returns {boolean}
13
+ */
14
+ export function hasCfgTables(db) {
15
+ try {
16
+ db.prepare('SELECT 1 FROM cfg_blocks LIMIT 0').get();
17
+ return true;
18
+ } catch {
19
+ return false;
20
+ }
21
+ }
22
+
23
+ /**
24
+ * Get CFG blocks for a function node.
25
+ * @param {object} db
26
+ * @param {number} functionNodeId
27
+ * @returns {object[]}
28
+ */
29
+ export function getCfgBlocks(db, 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);
37
+ }
38
+
39
+ /**
40
+ * Get CFG edges for a function node (with block info).
41
+ * @param {object} db
42
+ * @param {number} functionNodeId
43
+ * @returns {object[]}
44
+ */
45
+ export function getCfgEdges(db, 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);
58
+ }
59
+
60
+ /**
61
+ * Delete all CFG data for a function node.
62
+ * @param {object} db
63
+ * @param {number} functionNodeId
64
+ */
65
+ export function deleteCfgForNode(db, 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
+ );
72
+ }
@@ -0,0 +1,54 @@
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
+
8
+ /**
9
+ * Check whether the co_changes table has data.
10
+ * @param {object} db
11
+ * @returns {boolean}
12
+ */
13
+ export function hasCoChanges(db) {
14
+ try {
15
+ return !!cachedStmt(_hasCoChangesStmt, db, 'SELECT 1 FROM co_changes LIMIT 1').get();
16
+ } catch {
17
+ return false;
18
+ }
19
+ }
20
+
21
+ /**
22
+ * Get all co-change metadata as a key-value map.
23
+ * @param {object} db
24
+ * @returns {Record<string, string>}
25
+ */
26
+ export function getCoChangeMeta(db) {
27
+ const meta = {};
28
+ try {
29
+ for (const row of cachedStmt(
30
+ _getCoChangeMetaStmt,
31
+ db,
32
+ 'SELECT key, value FROM co_change_meta',
33
+ ).all()) {
34
+ meta[row.key] = row.value;
35
+ }
36
+ } catch {
37
+ /* table may not exist */
38
+ }
39
+ return meta;
40
+ }
41
+
42
+ /**
43
+ * Upsert a co-change metadata key-value pair.
44
+ * @param {object} db
45
+ * @param {string} key
46
+ * @param {string} value
47
+ */
48
+ export function upsertCoChangeMeta(db, key, value) {
49
+ cachedStmt(
50
+ _upsertCoChangeMetaStmt,
51
+ db,
52
+ 'INSERT INTO co_change_meta (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value',
53
+ ).run(key, value);
54
+ }
@@ -0,0 +1,20 @@
1
+ import { cachedStmt } from './cached-stmt.js';
2
+
3
+ // ─── Statement caches (one prepared statement per db instance) ────────────
4
+ const _getComplexityForNodeStmt = new WeakMap();
5
+
6
+ /**
7
+ * Get complexity metrics for a node.
8
+ * Used by contextData and explainFunctionImpl in queries.js.
9
+ * @param {object} db
10
+ * @param {number} nodeId
11
+ * @returns {{ cognitive: number, cyclomatic: number, max_nesting: number, maintainability_index: number, halstead_volume: number }|undefined}
12
+ */
13
+ export function getComplexityForNode(db, 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);
20
+ }
@@ -0,0 +1,17 @@
1
+ import { cachedStmt } from './cached-stmt.js';
2
+
3
+ // ─── Statement caches (one prepared statement per db instance) ────────────
4
+ const _hasDataflowTableStmt = new WeakMap();
5
+
6
+ /**
7
+ * Check whether the dataflow table exists and has data.
8
+ * @param {object} db
9
+ * @returns {boolean}
10
+ */
11
+ export function hasDataflowTable(db) {
12
+ try {
13
+ return cachedStmt(_hasDataflowTableStmt, db, 'SELECT COUNT(*) AS c FROM dataflow').get().c > 0;
14
+ } catch {
15
+ return false;
16
+ }
17
+ }
@@ -0,0 +1,281 @@
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
+
19
+ // ─── Call-edge queries ──────────────────────────────────────────────────
20
+
21
+ /**
22
+ * Find all callees of a node (outgoing 'calls' edges).
23
+ * Returns full node info including end_line for source display.
24
+ * @param {object} db
25
+ * @param {number} nodeId
26
+ * @returns {{ id: number, name: string, kind: string, file: string, line: number, end_line: number|null }[]}
27
+ */
28
+ export function findCallees(db, 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);
36
+ }
37
+
38
+ /**
39
+ * Find all callers of a node (incoming 'calls' edges).
40
+ * @param {object} db
41
+ * @param {number} nodeId
42
+ * @returns {{ id: number, name: string, kind: string, file: string, line: number }[]}
43
+ */
44
+ export function findCallers(db, 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);
52
+ }
53
+
54
+ /**
55
+ * Find distinct callers of a node (for impact analysis BFS).
56
+ * @param {object} db
57
+ * @param {number} nodeId
58
+ * @returns {{ id: number, name: string, kind: string, file: string, line: number }[]}
59
+ */
60
+ export function findDistinctCallers(db, 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);
68
+ }
69
+
70
+ // ─── All-edge queries (no kind filter) ─────────────────────────────────
71
+
72
+ /**
73
+ * Find all outgoing edges with edge kind (for queryNameData).
74
+ * @param {object} db
75
+ * @param {number} nodeId
76
+ * @returns {{ name: string, kind: string, file: string, line: number, edge_kind: string }[]}
77
+ */
78
+ export function findAllOutgoingEdges(db, 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);
86
+ }
87
+
88
+ /**
89
+ * Find all incoming edges with edge kind (for queryNameData).
90
+ * @param {object} db
91
+ * @param {number} nodeId
92
+ * @returns {{ name: string, kind: string, file: string, line: number, edge_kind: string }[]}
93
+ */
94
+ export function findAllIncomingEdges(db, 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);
102
+ }
103
+
104
+ // ─── Name-only callee/caller lookups (for embedder) ────────────────────
105
+
106
+ /**
107
+ * Get distinct callee names for a node, sorted alphabetically.
108
+ * @param {object} db
109
+ * @param {number} nodeId
110
+ * @returns {string[]}
111
+ */
112
+ export function findCalleeNames(db, nodeId) {
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
+ )
121
+ .all(nodeId)
122
+ .map((r) => r.name);
123
+ }
124
+
125
+ /**
126
+ * Get distinct caller names for a node, sorted alphabetically.
127
+ * @param {object} db
128
+ * @param {number} nodeId
129
+ * @returns {string[]}
130
+ */
131
+ export function findCallerNames(db, nodeId) {
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
+ )
140
+ .all(nodeId)
141
+ .map((r) => r.name);
142
+ }
143
+
144
+ // ─── Import-edge queries ───────────────────────────────────────────────
145
+
146
+ /**
147
+ * Find outgoing import edges (files this node imports).
148
+ * @param {object} db
149
+ * @param {number} nodeId
150
+ * @returns {{ file: string, edge_kind: string }[]}
151
+ */
152
+ export function findImportTargets(db, 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);
160
+ }
161
+
162
+ /**
163
+ * Find incoming import edges (files that import this node).
164
+ * @param {object} db
165
+ * @param {number} nodeId
166
+ * @returns {{ file: string, edge_kind: string }[]}
167
+ */
168
+ export function findImportSources(db, 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);
176
+ }
177
+
178
+ /**
179
+ * Find nodes that import a given node (BFS-ready, returns full node info).
180
+ * Used by impactAnalysisData for transitive import traversal.
181
+ * @param {object} db
182
+ * @param {number} nodeId
183
+ * @returns {object[]}
184
+ */
185
+ export function findImportDependents(db, 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);
192
+ }
193
+
194
+ // ─── Cross-file and hierarchy queries ──────────────────────────────────
195
+
196
+ /**
197
+ * Get IDs of symbols in a file that are called from other files.
198
+ * Used for "exported" detection in explain/where/exports.
199
+ * @param {object} db
200
+ * @param {string} file
201
+ * @returns {Set<number>}
202
+ */
203
+ export function findCrossFileCallTargets(db, file) {
204
+ return new Set(
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
+ )
213
+ .all(file, file)
214
+ .map((r) => r.target_id),
215
+ );
216
+ }
217
+
218
+ /**
219
+ * Count callers that are in a different file than the target node.
220
+ * Used by whereSymbolImpl to determine if a symbol is exported.
221
+ * @param {object} db
222
+ * @param {number} nodeId
223
+ * @param {string} file - The target node's file
224
+ * @returns {number}
225
+ */
226
+ export function countCrossFileCallers(db, nodeId, file) {
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;
233
+ }
234
+
235
+ /**
236
+ * Get all ancestor class IDs via extends edges (BFS).
237
+ * @param {object} db
238
+ * @param {number} classNodeId
239
+ * @returns {Set<number>}
240
+ */
241
+ export function getClassHierarchy(db, classNodeId) {
242
+ const ancestors = new Set();
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
+ );
250
+ while (queue.length > 0) {
251
+ const current = queue.shift();
252
+ const parents = stmt.all(current);
253
+ for (const p of parents) {
254
+ if (!ancestors.has(p.id)) {
255
+ ancestors.add(p.id);
256
+ queue.push(p.id);
257
+ }
258
+ }
259
+ }
260
+ return ancestors;
261
+ }
262
+
263
+ /**
264
+ * Find intra-file call edges (caller → callee within the same file).
265
+ * Used by explainFileImpl for data flow visualization.
266
+ * @param {object} db
267
+ * @param {string} file
268
+ * @returns {{ caller_name: string, callee_name: string }[]}
269
+ */
270
+ export function findIntraFileCallEdges(db, 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);
281
+ }
@@ -0,0 +1,51 @@
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
+
8
+ /**
9
+ * Check whether the embeddings table has data.
10
+ * @param {object} db
11
+ * @returns {boolean}
12
+ */
13
+ export function hasEmbeddings(db) {
14
+ try {
15
+ return !!cachedStmt(_hasEmbeddingsStmt, db, 'SELECT 1 FROM embeddings LIMIT 1').get();
16
+ } catch {
17
+ return false;
18
+ }
19
+ }
20
+
21
+ /**
22
+ * Get the count of embeddings.
23
+ * @param {object} db
24
+ * @returns {number}
25
+ */
26
+ export function getEmbeddingCount(db) {
27
+ try {
28
+ return cachedStmt(_getEmbeddingCountStmt, db, 'SELECT COUNT(*) AS c FROM embeddings').get().c;
29
+ } catch {
30
+ return 0;
31
+ }
32
+ }
33
+
34
+ /**
35
+ * Get a single embedding metadata value by key.
36
+ * @param {object} db
37
+ * @param {string} key
38
+ * @returns {string|undefined}
39
+ */
40
+ export function getEmbeddingMeta(db, key) {
41
+ try {
42
+ const row = cachedStmt(
43
+ _getEmbeddingMetaStmt,
44
+ db,
45
+ 'SELECT value FROM embedding_meta WHERE key = ?',
46
+ ).get(key);
47
+ return row?.value;
48
+ } catch {
49
+ return undefined;
50
+ }
51
+ }
@@ -0,0 +1,59 @@
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
+
9
+ /**
10
+ * Get callable nodes (function/method/class) for community detection.
11
+ * @param {object} db
12
+ * @returns {{ id: number, name: string, kind: string, file: string }[]}
13
+ */
14
+ export function getCallableNodes(db) {
15
+ return cachedStmt(
16
+ _getCallableNodesStmt,
17
+ db,
18
+ "SELECT id, name, kind, file FROM nodes WHERE kind IN ('function','method','class')",
19
+ ).all();
20
+ }
21
+
22
+ /**
23
+ * Get all 'calls' edges.
24
+ * @param {object} db
25
+ * @returns {{ source_id: number, target_id: number }[]}
26
+ */
27
+ export function getCallEdges(db) {
28
+ return cachedStmt(
29
+ _getCallEdgesStmt,
30
+ db,
31
+ "SELECT source_id, target_id FROM edges WHERE kind = 'calls'",
32
+ ).all();
33
+ }
34
+
35
+ /**
36
+ * Get all file-kind nodes.
37
+ * @param {object} db
38
+ * @returns {{ id: number, name: string, file: string }[]}
39
+ */
40
+ export function getFileNodesAll(db) {
41
+ return cachedStmt(
42
+ _getFileNodesAllStmt,
43
+ db,
44
+ "SELECT id, name, file FROM nodes WHERE kind = 'file'",
45
+ ).all();
46
+ }
47
+
48
+ /**
49
+ * Get all import edges.
50
+ * @param {object} db
51
+ * @returns {{ source_id: number, target_id: number }[]}
52
+ */
53
+ export function getImportEdges(db) {
54
+ return cachedStmt(
55
+ _getImportEdgesStmt,
56
+ db,
57
+ "SELECT source_id, target_id FROM edges WHERE kind IN ('imports','imports-type')",
58
+ ).all();
59
+ }