@optave/codegraph 1.1.0 → 1.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/queries.js CHANGED
@@ -1,9 +1,6 @@
1
-
2
- import Database from 'better-sqlite3';
3
- import path from 'path';
4
- import { execFileSync } from 'child_process';
1
+ import { execFileSync } from 'node:child_process';
2
+ import path from 'node:path';
5
3
  import { findDbPath, openReadonlyOrFail } from './db.js';
6
- import { warn, debug } from './logger.js';
7
4
 
8
5
  const TEST_PATTERN = /\.(test|spec)\.|__test__|__tests__|\.stories\./;
9
6
  function isTestFile(filePath) {
@@ -18,10 +15,12 @@ function getClassHierarchy(db, classNodeId) {
18
15
  const queue = [classNodeId];
19
16
  while (queue.length > 0) {
20
17
  const current = queue.shift();
21
- const parents = db.prepare(`
18
+ const parents = db
19
+ .prepare(`
22
20
  SELECT n.id, n.name FROM edges e JOIN nodes n ON e.target_id = n.id
23
21
  WHERE e.source_id = ? AND e.kind = 'extends'
24
- `).all(current);
22
+ `)
23
+ .all(current);
25
24
  for (const p of parents) {
26
25
  if (!ancestors.has(p.id)) {
27
26
  ancestors.add(p.id);
@@ -33,25 +32,25 @@ function getClassHierarchy(db, classNodeId) {
33
32
  }
34
33
 
35
34
  function resolveMethodViaHierarchy(db, methodName) {
36
- const methods = db.prepare(
37
- `SELECT * FROM nodes WHERE kind = 'method' AND name LIKE ?`
38
- ).all(`%.${methodName}`);
35
+ const methods = db
36
+ .prepare(`SELECT * FROM nodes WHERE kind = 'method' AND name LIKE ?`)
37
+ .all(`%.${methodName}`);
39
38
 
40
39
  const results = [...methods];
41
40
  for (const m of methods) {
42
41
  const className = m.name.split('.')[0];
43
- const classNode = db.prepare(
44
- `SELECT * FROM nodes WHERE name = ? AND kind = 'class' AND file = ?`
45
- ).get(className, m.file);
42
+ const classNode = db
43
+ .prepare(`SELECT * FROM nodes WHERE name = ? AND kind = 'class' AND file = ?`)
44
+ .get(className, m.file);
46
45
  if (!classNode) continue;
47
46
 
48
47
  const ancestors = getClassHierarchy(db, classNode.id);
49
48
  for (const ancestorId of ancestors) {
50
49
  const ancestor = db.prepare('SELECT name FROM nodes WHERE id = ?').get(ancestorId);
51
50
  if (!ancestor) continue;
52
- const parentMethods = db.prepare(
53
- `SELECT * FROM nodes WHERE name = ? AND kind = 'method'`
54
- ).all(`${ancestor.name}.${methodName}`);
51
+ const parentMethods = db
52
+ .prepare(`SELECT * FROM nodes WHERE name = ? AND kind = 'method'`)
53
+ .all(`${ancestor.name}.${methodName}`);
55
54
  results.push(...parentMethods);
56
55
  }
57
56
  }
@@ -60,13 +59,20 @@ function resolveMethodViaHierarchy(db, methodName) {
60
59
 
61
60
  function kindIcon(kind) {
62
61
  switch (kind) {
63
- case 'function': return 'f';
64
- case 'class': return '*';
65
- case 'method': return 'o';
66
- case 'file': return '#';
67
- case 'interface': return 'I';
68
- case 'type': return 'T';
69
- default: return '-';
62
+ case 'function':
63
+ return 'f';
64
+ case 'class':
65
+ return '*';
66
+ case 'method':
67
+ return 'o';
68
+ case 'file':
69
+ return '#';
70
+ case 'interface':
71
+ return 'I';
72
+ case 'type':
73
+ return 'T';
74
+ default:
75
+ return '-';
70
76
  }
71
77
  }
72
78
 
@@ -75,25 +81,47 @@ function kindIcon(kind) {
75
81
  export function queryNameData(name, customDbPath) {
76
82
  const db = openReadonlyOrFail(customDbPath);
77
83
  const nodes = db.prepare(`SELECT * FROM nodes WHERE name LIKE ?`).all(`%${name}%`);
78
- if (nodes.length === 0) { db.close(); return { query: name, results: [] }; }
84
+ if (nodes.length === 0) {
85
+ db.close();
86
+ return { query: name, results: [] };
87
+ }
79
88
 
80
- const results = nodes.map(node => {
81
- const callees = db.prepare(`
89
+ const results = nodes.map((node) => {
90
+ const callees = db
91
+ .prepare(`
82
92
  SELECT n.name, n.kind, n.file, n.line, e.kind as edge_kind
83
93
  FROM edges e JOIN nodes n ON e.target_id = n.id
84
94
  WHERE e.source_id = ?
85
- `).all(node.id);
95
+ `)
96
+ .all(node.id);
86
97
 
87
- const callers = db.prepare(`
98
+ const callers = db
99
+ .prepare(`
88
100
  SELECT n.name, n.kind, n.file, n.line, e.kind as edge_kind
89
101
  FROM edges e JOIN nodes n ON e.source_id = n.id
90
102
  WHERE e.target_id = ?
91
- `).all(node.id);
103
+ `)
104
+ .all(node.id);
92
105
 
93
106
  return {
94
- name: node.name, kind: node.kind, file: node.file, line: node.line,
95
- callees: callees.map(c => ({ name: c.name, kind: c.kind, file: c.file, line: c.line, edgeKind: c.edge_kind })),
96
- callers: callers.map(c => ({ name: c.name, kind: c.kind, file: c.file, line: c.line, edgeKind: c.edge_kind })),
107
+ name: node.name,
108
+ kind: node.kind,
109
+ file: node.file,
110
+ line: node.line,
111
+ callees: callees.map((c) => ({
112
+ name: c.name,
113
+ kind: c.kind,
114
+ file: c.file,
115
+ line: c.line,
116
+ edgeKind: c.edge_kind,
117
+ })),
118
+ callers: callers.map((c) => ({
119
+ name: c.name,
120
+ kind: c.kind,
121
+ file: c.file,
122
+ line: c.line,
123
+ edgeKind: c.edge_kind,
124
+ })),
97
125
  };
98
126
  });
99
127
 
@@ -103,8 +131,13 @@ export function queryNameData(name, customDbPath) {
103
131
 
104
132
  export function impactAnalysisData(file, customDbPath) {
105
133
  const db = openReadonlyOrFail(customDbPath);
106
- const fileNodes = db.prepare(`SELECT * FROM nodes WHERE file LIKE ? AND kind = 'file'`).all(`%${file}%`);
107
- if (fileNodes.length === 0) { db.close(); return { file, sources: [], levels: {}, totalDependents: 0 }; }
134
+ const fileNodes = db
135
+ .prepare(`SELECT * FROM nodes WHERE file LIKE ? AND kind = 'file'`)
136
+ .all(`%${file}%`);
137
+ if (fileNodes.length === 0) {
138
+ db.close();
139
+ return { file, sources: [], levels: {}, totalDependents: 0 };
140
+ }
108
141
 
109
142
  const visited = new Set();
110
143
  const queue = [];
@@ -119,10 +152,12 @@ export function impactAnalysisData(file, customDbPath) {
119
152
  while (queue.length > 0) {
120
153
  const current = queue.shift();
121
154
  const level = levels.get(current);
122
- const dependents = db.prepare(`
155
+ const dependents = db
156
+ .prepare(`
123
157
  SELECT n.* FROM edges e JOIN nodes n ON e.source_id = n.id
124
158
  WHERE e.target_id = ? AND e.kind IN ('imports', 'imports-type')
125
- `).all(current);
159
+ `)
160
+ .all(current);
126
161
  for (const dep of dependents) {
127
162
  if (!visited.has(dep.id)) {
128
163
  visited.add(dep.id);
@@ -143,7 +178,7 @@ export function impactAnalysisData(file, customDbPath) {
143
178
  db.close();
144
179
  return {
145
180
  file,
146
- sources: fileNodes.map(f => f.file),
181
+ sources: fileNodes.map((f) => f.file),
147
182
  levels: byLevel,
148
183
  totalDependents: visited.size - fileNodes.length,
149
184
  };
@@ -152,7 +187,8 @@ export function impactAnalysisData(file, customDbPath) {
152
187
  export function moduleMapData(customDbPath, limit = 20) {
153
188
  const db = openReadonlyOrFail(customDbPath);
154
189
 
155
- const nodes = db.prepare(`
190
+ const nodes = db
191
+ .prepare(`
156
192
  SELECT n.*,
157
193
  (SELECT COUNT(*) FROM edges WHERE source_id = n.id) as out_edges,
158
194
  (SELECT COUNT(*) FROM edges WHERE target_id = n.id) as in_edges
@@ -163,9 +199,10 @@ export function moduleMapData(customDbPath, limit = 20) {
163
199
  AND n.file NOT LIKE '%__test__%'
164
200
  ORDER BY (SELECT COUNT(*) FROM edges WHERE target_id = n.id) DESC
165
201
  LIMIT ?
166
- `).all(limit);
202
+ `)
203
+ .all(limit);
167
204
 
168
- const topNodes = nodes.map(n => ({
205
+ const topNodes = nodes.map((n) => ({
169
206
  file: n.file,
170
207
  dir: path.dirname(n.file) || '.',
171
208
  inEdges: n.in_edges,
@@ -182,27 +219,38 @@ export function moduleMapData(customDbPath, limit = 20) {
182
219
 
183
220
  export function fileDepsData(file, customDbPath) {
184
221
  const db = openReadonlyOrFail(customDbPath);
185
- const fileNodes = db.prepare(`SELECT * FROM nodes WHERE file LIKE ? AND kind = 'file'`).all(`%${file}%`);
186
- if (fileNodes.length === 0) { db.close(); return { file, results: [] }; }
222
+ const fileNodes = db
223
+ .prepare(`SELECT * FROM nodes WHERE file LIKE ? AND kind = 'file'`)
224
+ .all(`%${file}%`);
225
+ if (fileNodes.length === 0) {
226
+ db.close();
227
+ return { file, results: [] };
228
+ }
187
229
 
188
- const results = fileNodes.map(fn => {
189
- const importsTo = db.prepare(`
230
+ const results = fileNodes.map((fn) => {
231
+ const importsTo = db
232
+ .prepare(`
190
233
  SELECT n.file, e.kind as edge_kind FROM edges e JOIN nodes n ON e.target_id = n.id
191
234
  WHERE e.source_id = ? AND e.kind IN ('imports', 'imports-type')
192
- `).all(fn.id);
235
+ `)
236
+ .all(fn.id);
193
237
 
194
- const importedBy = db.prepare(`
238
+ const importedBy = db
239
+ .prepare(`
195
240
  SELECT n.file, e.kind as edge_kind FROM edges e JOIN nodes n ON e.source_id = n.id
196
241
  WHERE e.target_id = ? AND e.kind IN ('imports', 'imports-type')
197
- `).all(fn.id);
242
+ `)
243
+ .all(fn.id);
198
244
 
199
- const defs = db.prepare(`SELECT * FROM nodes WHERE file = ? AND kind != 'file' ORDER BY line`).all(fn.file);
245
+ const defs = db
246
+ .prepare(`SELECT * FROM nodes WHERE file = ? AND kind != 'file' ORDER BY line`)
247
+ .all(fn.file);
200
248
 
201
249
  return {
202
250
  file: fn.file,
203
- imports: importsTo.map(i => ({ file: i.file, typeOnly: i.edge_kind === 'imports-type' })),
204
- importedBy: importedBy.map(i => ({ file: i.file })),
205
- definitions: defs.map(d => ({ name: d.name, kind: d.kind, line: d.line })),
251
+ imports: importsTo.map((i) => ({ file: i.file, typeOnly: i.edge_kind === 'imports-type' })),
252
+ importedBy: importedBy.map((i) => ({ file: i.file })),
253
+ definitions: defs.map((d) => ({ name: d.name, kind: d.kind, line: d.line })),
206
254
  };
207
255
  });
208
256
 
@@ -215,70 +263,94 @@ export function fnDepsData(name, customDbPath, opts = {}) {
215
263
  const depth = opts.depth || 3;
216
264
  const noTests = opts.noTests || false;
217
265
 
218
- let nodes = db.prepare(
219
- `SELECT * FROM nodes WHERE name LIKE ? AND kind IN ('function', 'method', 'class') ORDER BY file, line`
220
- ).all(`%${name}%`);
221
- if (noTests) nodes = nodes.filter(n => !isTestFile(n.file));
222
- if (nodes.length === 0) { db.close(); return { name, results: [] }; }
266
+ let nodes = db
267
+ .prepare(
268
+ `SELECT * FROM nodes WHERE name LIKE ? AND kind IN ('function', 'method', 'class') ORDER BY file, line`,
269
+ )
270
+ .all(`%${name}%`);
271
+ if (noTests) nodes = nodes.filter((n) => !isTestFile(n.file));
272
+ if (nodes.length === 0) {
273
+ db.close();
274
+ return { name, results: [] };
275
+ }
223
276
 
224
- const results = nodes.map(node => {
225
- const callees = db.prepare(`
277
+ const results = nodes.map((node) => {
278
+ const callees = db
279
+ .prepare(`
226
280
  SELECT n.name, n.kind, n.file, n.line, e.kind as edge_kind
227
281
  FROM edges e JOIN nodes n ON e.target_id = n.id
228
282
  WHERE e.source_id = ? AND e.kind = 'calls'
229
- `).all(node.id);
230
- let filteredCallees = noTests ? callees.filter(c => !isTestFile(c.file)) : callees;
283
+ `)
284
+ .all(node.id);
285
+ const filteredCallees = noTests ? callees.filter((c) => !isTestFile(c.file)) : callees;
231
286
 
232
- let callers = db.prepare(`
287
+ let callers = db
288
+ .prepare(`
233
289
  SELECT n.name, n.kind, n.file, n.line, e.kind as edge_kind
234
290
  FROM edges e JOIN nodes n ON e.source_id = n.id
235
291
  WHERE e.target_id = ? AND e.kind = 'calls'
236
- `).all(node.id);
292
+ `)
293
+ .all(node.id);
237
294
 
238
295
  if (node.kind === 'method' && node.name.includes('.')) {
239
296
  const methodName = node.name.split('.').pop();
240
297
  const relatedMethods = resolveMethodViaHierarchy(db, methodName);
241
298
  for (const rm of relatedMethods) {
242
299
  if (rm.id === node.id) continue;
243
- const extraCallers = db.prepare(`
300
+ const extraCallers = db
301
+ .prepare(`
244
302
  SELECT n.name, n.kind, n.file, n.line, e.kind as edge_kind
245
303
  FROM edges e JOIN nodes n ON e.source_id = n.id
246
304
  WHERE e.target_id = ? AND e.kind = 'calls'
247
- `).all(rm.id);
248
- callers.push(...extraCallers.map(c => ({ ...c, viaHierarchy: rm.name })));
305
+ `)
306
+ .all(rm.id);
307
+ callers.push(...extraCallers.map((c) => ({ ...c, viaHierarchy: rm.name })));
249
308
  }
250
309
  }
251
- if (noTests) callers = callers.filter(c => !isTestFile(c.file));
310
+ if (noTests) callers = callers.filter((c) => !isTestFile(c.file));
252
311
 
253
312
  // Transitive callers
254
313
  const transitiveCallers = {};
255
314
  if (depth > 1) {
256
315
  const visited = new Set([node.id]);
257
- let frontier = callers.map(c => {
258
- const row = db.prepare('SELECT id FROM nodes WHERE name = ? AND kind = ? AND file = ? AND line = ?').get(c.name, c.kind, c.file, c.line);
259
- return row ? { ...c, id: row.id } : null;
260
- }).filter(Boolean);
316
+ let frontier = callers
317
+ .map((c) => {
318
+ const row = db
319
+ .prepare('SELECT id FROM nodes WHERE name = ? AND kind = ? AND file = ? AND line = ?')
320
+ .get(c.name, c.kind, c.file, c.line);
321
+ return row ? { ...c, id: row.id } : null;
322
+ })
323
+ .filter(Boolean);
261
324
 
262
325
  for (let d = 2; d <= depth; d++) {
263
326
  const nextFrontier = [];
264
327
  for (const f of frontier) {
265
328
  if (visited.has(f.id)) continue;
266
329
  visited.add(f.id);
267
- const upstream = db.prepare(`
330
+ const upstream = db
331
+ .prepare(`
268
332
  SELECT n.name, n.kind, n.file, n.line
269
333
  FROM edges e JOIN nodes n ON e.source_id = n.id
270
334
  WHERE e.target_id = ? AND e.kind = 'calls'
271
- `).all(f.id);
335
+ `)
336
+ .all(f.id);
272
337
  for (const u of upstream) {
273
338
  if (noTests && isTestFile(u.file)) continue;
274
- const uid = db.prepare('SELECT id FROM nodes WHERE name = ? AND kind = ? AND file = ? AND line = ?').get(u.name, u.kind, u.file, u.line)?.id;
339
+ const uid = db
340
+ .prepare('SELECT id FROM nodes WHERE name = ? AND kind = ? AND file = ? AND line = ?')
341
+ .get(u.name, u.kind, u.file, u.line)?.id;
275
342
  if (uid && !visited.has(uid)) {
276
343
  nextFrontier.push({ ...u, id: uid });
277
344
  }
278
345
  }
279
346
  }
280
347
  if (nextFrontier.length > 0) {
281
- transitiveCallers[d] = nextFrontier.map(n => ({ name: n.name, kind: n.kind, file: n.file, line: n.line }));
348
+ transitiveCallers[d] = nextFrontier.map((n) => ({
349
+ name: n.name,
350
+ kind: n.kind,
351
+ file: n.file,
352
+ line: n.line,
353
+ }));
282
354
  }
283
355
  frontier = nextFrontier;
284
356
  if (frontier.length === 0) break;
@@ -286,9 +358,23 @@ export function fnDepsData(name, customDbPath, opts = {}) {
286
358
  }
287
359
 
288
360
  return {
289
- name: node.name, kind: node.kind, file: node.file, line: node.line,
290
- callees: filteredCallees.map(c => ({ name: c.name, kind: c.kind, file: c.file, line: c.line })),
291
- callers: callers.map(c => ({ name: c.name, kind: c.kind, file: c.file, line: c.line, viaHierarchy: c.viaHierarchy || undefined })),
361
+ name: node.name,
362
+ kind: node.kind,
363
+ file: node.file,
364
+ line: node.line,
365
+ callees: filteredCallees.map((c) => ({
366
+ name: c.name,
367
+ kind: c.kind,
368
+ file: c.file,
369
+ line: c.line,
370
+ })),
371
+ callers: callers.map((c) => ({
372
+ name: c.name,
373
+ kind: c.kind,
374
+ file: c.file,
375
+ line: c.line,
376
+ viaHierarchy: c.viaHierarchy || undefined,
377
+ })),
292
378
  transitiveCallers,
293
379
  };
294
380
  });
@@ -302,13 +388,16 @@ export function fnImpactData(name, customDbPath, opts = {}) {
302
388
  const maxDepth = opts.depth || 5;
303
389
  const noTests = opts.noTests || false;
304
390
 
305
- let nodes = db.prepare(
306
- `SELECT * FROM nodes WHERE name LIKE ? AND kind IN ('function', 'method', 'class')`
307
- ).all(`%${name}%`);
308
- if (noTests) nodes = nodes.filter(n => !isTestFile(n.file));
309
- if (nodes.length === 0) { db.close(); return { name, results: [] }; }
391
+ let nodes = db
392
+ .prepare(`SELECT * FROM nodes WHERE name LIKE ? AND kind IN ('function', 'method', 'class')`)
393
+ .all(`%${name}%`);
394
+ if (noTests) nodes = nodes.filter((n) => !isTestFile(n.file));
395
+ if (nodes.length === 0) {
396
+ db.close();
397
+ return { name, results: [] };
398
+ }
310
399
 
311
- const results = nodes.slice(0, 3).map(node => {
400
+ const results = nodes.slice(0, 3).map((node) => {
312
401
  const visited = new Set([node.id]);
313
402
  const levels = {};
314
403
  let frontier = [node.id];
@@ -316,11 +405,13 @@ export function fnImpactData(name, customDbPath, opts = {}) {
316
405
  for (let d = 1; d <= maxDepth; d++) {
317
406
  const nextFrontier = [];
318
407
  for (const fid of frontier) {
319
- const callers = db.prepare(`
408
+ const callers = db
409
+ .prepare(`
320
410
  SELECT DISTINCT n.id, n.name, n.kind, n.file, n.line
321
411
  FROM edges e JOIN nodes n ON e.source_id = n.id
322
412
  WHERE e.target_id = ? AND e.kind = 'calls'
323
- `).all(fid);
413
+ `)
414
+ .all(fid);
324
415
  for (const c of callers) {
325
416
  if (!visited.has(c.id) && (!noTests || !isTestFile(c.file))) {
326
417
  visited.add(c.id);
@@ -335,7 +426,10 @@ export function fnImpactData(name, customDbPath, opts = {}) {
335
426
  }
336
427
 
337
428
  return {
338
- name: node.name, kind: node.kind, file: node.file, line: node.line,
429
+ name: node.name,
430
+ kind: node.kind,
431
+ file: node.file,
432
+ line: node.line,
339
433
  levels,
340
434
  totalDependents: visited.size - 1,
341
435
  };
@@ -366,36 +460,48 @@ export function diffImpactData(customDbPath, opts = {}) {
366
460
  diffOutput = execFileSync('git', args, {
367
461
  cwd: repoRoot,
368
462
  encoding: 'utf-8',
369
- maxBuffer: 10 * 1024 * 1024
463
+ maxBuffer: 10 * 1024 * 1024,
370
464
  });
371
465
  } catch (e) {
372
466
  db.close();
373
467
  return { error: `Failed to run git diff: ${e.message}` };
374
468
  }
375
469
 
376
- if (!diffOutput.trim()) { db.close(); return { changedFiles: 0, affectedFunctions: [], affectedFiles: [], summary: null }; }
470
+ if (!diffOutput.trim()) {
471
+ db.close();
472
+ return { changedFiles: 0, affectedFunctions: [], affectedFiles: [], summary: null };
473
+ }
377
474
 
378
475
  const changedRanges = new Map();
379
476
  let currentFile = null;
380
477
  for (const line of diffOutput.split('\n')) {
381
478
  const fileMatch = line.match(/^\+\+\+ b\/(.+)/);
382
- if (fileMatch) { currentFile = fileMatch[1]; if (!changedRanges.has(currentFile)) changedRanges.set(currentFile, []); continue; }
479
+ if (fileMatch) {
480
+ currentFile = fileMatch[1];
481
+ if (!changedRanges.has(currentFile)) changedRanges.set(currentFile, []);
482
+ continue;
483
+ }
383
484
  const hunkMatch = line.match(/^@@ .+ \+(\d+)(?:,(\d+))? @@/);
384
485
  if (hunkMatch && currentFile) {
385
- const start = parseInt(hunkMatch[1]);
386
- const count = parseInt(hunkMatch[2] || '1');
486
+ const start = parseInt(hunkMatch[1], 10);
487
+ const count = parseInt(hunkMatch[2] || '1', 10);
387
488
  changedRanges.get(currentFile).push({ start, end: start + count - 1 });
388
489
  }
389
490
  }
390
491
 
391
- if (changedRanges.size === 0) { db.close(); return { changedFiles: 0, affectedFunctions: [], affectedFiles: [], summary: null }; }
492
+ if (changedRanges.size === 0) {
493
+ db.close();
494
+ return { changedFiles: 0, affectedFunctions: [], affectedFiles: [], summary: null };
495
+ }
392
496
 
393
497
  const affectedFunctions = [];
394
498
  for (const [file, ranges] of changedRanges) {
395
499
  if (noTests && isTestFile(file)) continue;
396
- const defs = db.prepare(
397
- `SELECT * FROM nodes WHERE file = ? AND kind IN ('function', 'method', 'class') ORDER BY line`
398
- ).all(file);
500
+ const defs = db
501
+ .prepare(
502
+ `SELECT * FROM nodes WHERE file = ? AND kind IN ('function', 'method', 'class') ORDER BY line`,
503
+ )
504
+ .all(file);
399
505
  for (let i = 0; i < defs.length; i++) {
400
506
  const def = defs[i];
401
507
  const endLine = def.end_line || (defs[i + 1] ? defs[i + 1].line - 1 : 999999);
@@ -409,18 +515,20 @@ export function diffImpactData(customDbPath, opts = {}) {
409
515
  }
410
516
 
411
517
  const allAffected = new Set();
412
- const functionResults = affectedFunctions.map(fn => {
518
+ const functionResults = affectedFunctions.map((fn) => {
413
519
  const visited = new Set([fn.id]);
414
520
  let frontier = [fn.id];
415
521
  let totalCallers = 0;
416
522
  for (let d = 1; d <= maxDepth; d++) {
417
523
  const nextFrontier = [];
418
524
  for (const fid of frontier) {
419
- const callers = db.prepare(`
525
+ const callers = db
526
+ .prepare(`
420
527
  SELECT DISTINCT n.id, n.name, n.kind, n.file, n.line
421
528
  FROM edges e JOIN nodes n ON e.source_id = n.id
422
529
  WHERE e.target_id = ? AND e.kind = 'calls'
423
- `).all(fid);
530
+ `)
531
+ .all(fid);
424
532
  for (const c of callers) {
425
533
  if (!visited.has(c.id) && (!noTests || !isTestFile(c.file))) {
426
534
  visited.add(c.id);
@@ -433,7 +541,13 @@ export function diffImpactData(customDbPath, opts = {}) {
433
541
  frontier = nextFrontier;
434
542
  if (frontier.length === 0) break;
435
543
  }
436
- return { name: fn.name, kind: fn.kind, file: fn.file, line: fn.line, transitiveCallers: totalCallers };
544
+ return {
545
+ name: fn.name,
546
+ kind: fn.kind,
547
+ file: fn.file,
548
+ line: fn.line,
549
+ transitiveCallers: totalCallers,
550
+ };
437
551
  });
438
552
 
439
553
  const affectedFiles = new Set();
@@ -452,24 +566,62 @@ export function diffImpactData(customDbPath, opts = {}) {
452
566
  };
453
567
  }
454
568
 
569
+ export function listFunctionsData(customDbPath, opts = {}) {
570
+ const db = openReadonlyOrFail(customDbPath);
571
+ const noTests = opts.noTests || false;
572
+ const kinds = ['function', 'method', 'class'];
573
+ const placeholders = kinds.map(() => '?').join(', ');
574
+
575
+ const conditions = [`kind IN (${placeholders})`];
576
+ const params = [...kinds];
577
+
578
+ if (opts.file) {
579
+ conditions.push('file LIKE ?');
580
+ params.push(`%${opts.file}%`);
581
+ }
582
+ if (opts.pattern) {
583
+ conditions.push('name LIKE ?');
584
+ params.push(`%${opts.pattern}%`);
585
+ }
586
+
587
+ let rows = db
588
+ .prepare(
589
+ `SELECT name, kind, file, line FROM nodes WHERE ${conditions.join(' AND ')} ORDER BY file, line`,
590
+ )
591
+ .all(...params);
592
+
593
+ if (noTests) rows = rows.filter((r) => !isTestFile(r.file));
594
+
595
+ db.close();
596
+ return { count: rows.length, functions: rows };
597
+ }
598
+
455
599
  // ─── Human-readable output (original formatting) ───────────────────────
456
600
 
457
601
  export function queryName(name, customDbPath, opts = {}) {
458
602
  const data = queryNameData(name, customDbPath);
459
- if (opts.json) { console.log(JSON.stringify(data, null, 2)); return; }
460
- if (data.results.length === 0) { console.log(`No results for "${name}"`); return; }
603
+ if (opts.json) {
604
+ console.log(JSON.stringify(data, null, 2));
605
+ return;
606
+ }
607
+ if (data.results.length === 0) {
608
+ console.log(`No results for "${name}"`);
609
+ return;
610
+ }
461
611
 
462
612
  console.log(`\nResults for "${name}":\n`);
463
613
  for (const r of data.results) {
464
614
  console.log(` ${kindIcon(r.kind)} ${r.name} (${r.kind}) -- ${r.file}:${r.line}`);
465
615
  if (r.callees.length > 0) {
466
616
  console.log(` -> calls/uses:`);
467
- for (const c of r.callees.slice(0, 15)) console.log(` -> ${c.name} (${c.edgeKind}) ${c.file}:${c.line}`);
617
+ for (const c of r.callees.slice(0, 15))
618
+ console.log(` -> ${c.name} (${c.edgeKind}) ${c.file}:${c.line}`);
468
619
  if (r.callees.length > 15) console.log(` ... and ${r.callees.length - 15} more`);
469
620
  }
470
621
  if (r.callers.length > 0) {
471
622
  console.log(` <- called by:`);
472
- for (const c of r.callers.slice(0, 15)) console.log(` <- ${c.name} (${c.edgeKind}) ${c.file}:${c.line}`);
623
+ for (const c of r.callers.slice(0, 15))
624
+ console.log(` <- ${c.name} (${c.edgeKind}) ${c.file}:${c.line}`);
473
625
  if (r.callers.length > 15) console.log(` ... and ${r.callers.length - 15} more`);
474
626
  }
475
627
  console.log();
@@ -478,8 +630,14 @@ export function queryName(name, customDbPath, opts = {}) {
478
630
 
479
631
  export function impactAnalysis(file, customDbPath, opts = {}) {
480
632
  const data = impactAnalysisData(file, customDbPath);
481
- if (opts.json) { console.log(JSON.stringify(data, null, 2)); return; }
482
- if (data.sources.length === 0) { console.log(`No file matching "${file}" in graph`); return; }
633
+ if (opts.json) {
634
+ console.log(JSON.stringify(data, null, 2));
635
+ return;
636
+ }
637
+ if (data.sources.length === 0) {
638
+ console.log(`No file matching "${file}" in graph`);
639
+ return;
640
+ }
483
641
 
484
642
  console.log(`\nImpact analysis for files matching "${file}":\n`);
485
643
  for (const s of data.sources) console.log(` # ${s} (source)`);
@@ -490,8 +648,11 @@ export function impactAnalysis(file, customDbPath, opts = {}) {
490
648
  } else {
491
649
  for (const level of Object.keys(levels).sort((a, b) => a - b)) {
492
650
  const nodes = levels[level];
493
- console.log(`\n ${'--'.repeat(parseInt(level))} Level ${level} (${nodes.length} files):`);
494
- for (const n of nodes.slice(0, 30)) console.log(` ${' '.repeat(parseInt(level))}^ ${n.file}`);
651
+ console.log(
652
+ `\n ${'--'.repeat(parseInt(level, 10))} Level ${level} (${nodes.length} files):`,
653
+ );
654
+ for (const n of nodes.slice(0, 30))
655
+ console.log(` ${' '.repeat(parseInt(level, 10))}^ ${n.file}`);
495
656
  if (nodes.length > 30) console.log(` ... and ${nodes.length - 30} more`);
496
657
  }
497
658
  }
@@ -500,7 +661,10 @@ export function impactAnalysis(file, customDbPath, opts = {}) {
500
661
 
501
662
  export function moduleMap(customDbPath, limit = 20, opts = {}) {
502
663
  const data = moduleMapData(customDbPath, limit);
503
- if (opts.json) { console.log(JSON.stringify(data, null, 2)); return; }
664
+ if (opts.json) {
665
+ console.log(JSON.stringify(data, null, 2));
666
+ return;
667
+ }
504
668
 
505
669
  console.log(`\nModule map (top ${limit} most-connected nodes):\n`);
506
670
  const dirs = new Map();
@@ -513,16 +677,26 @@ export function moduleMap(customDbPath, limit = 20, opts = {}) {
513
677
  for (const f of files) {
514
678
  const total = f.inEdges + f.outEdges;
515
679
  const bar = '#'.repeat(Math.min(total, 40));
516
- console.log(` ${path.basename(f.file).padEnd(35)} <-${String(f.inEdges).padStart(3)} ->${String(f.outEdges).padStart(3)} ${bar}`);
680
+ console.log(
681
+ ` ${path.basename(f.file).padEnd(35)} <-${String(f.inEdges).padStart(3)} ->${String(f.outEdges).padStart(3)} ${bar}`,
682
+ );
517
683
  }
518
684
  }
519
- console.log(`\n Total: ${data.stats.totalFiles} files, ${data.stats.totalNodes} symbols, ${data.stats.totalEdges} edges\n`);
685
+ console.log(
686
+ `\n Total: ${data.stats.totalFiles} files, ${data.stats.totalNodes} symbols, ${data.stats.totalEdges} edges\n`,
687
+ );
520
688
  }
521
689
 
522
690
  export function fileDeps(file, customDbPath, opts = {}) {
523
691
  const data = fileDepsData(file, customDbPath);
524
- if (opts.json) { console.log(JSON.stringify(data, null, 2)); return; }
525
- if (data.results.length === 0) { console.log(`No file matching "${file}" in graph`); return; }
692
+ if (opts.json) {
693
+ console.log(JSON.stringify(data, null, 2));
694
+ return;
695
+ }
696
+ if (data.results.length === 0) {
697
+ console.log(`No file matching "${file}" in graph`);
698
+ return;
699
+ }
526
700
 
527
701
  for (const r of data.results) {
528
702
  console.log(`\n# ${r.file}\n`);
@@ -535,7 +709,8 @@ export function fileDeps(file, customDbPath, opts = {}) {
535
709
  for (const i of r.importedBy) console.log(` <- ${i.file}`);
536
710
  if (r.definitions.length > 0) {
537
711
  console.log(`\n Definitions (${r.definitions.length}):`);
538
- for (const d of r.definitions.slice(0, 30)) console.log(` ${kindIcon(d.kind)} ${d.name} :${d.line}`);
712
+ for (const d of r.definitions.slice(0, 30))
713
+ console.log(` ${kindIcon(d.kind)} ${d.name} :${d.line}`);
539
714
  if (r.definitions.length > 30) console.log(` ... and ${r.definitions.length - 30} more`);
540
715
  }
541
716
  console.log();
@@ -544,14 +719,21 @@ export function fileDeps(file, customDbPath, opts = {}) {
544
719
 
545
720
  export function fnDeps(name, customDbPath, opts = {}) {
546
721
  const data = fnDepsData(name, customDbPath, opts);
547
- if (opts.json) { console.log(JSON.stringify(data, null, 2)); return; }
548
- if (data.results.length === 0) { console.log(`No function/method/class matching "${name}"`); return; }
722
+ if (opts.json) {
723
+ console.log(JSON.stringify(data, null, 2));
724
+ return;
725
+ }
726
+ if (data.results.length === 0) {
727
+ console.log(`No function/method/class matching "${name}"`);
728
+ return;
729
+ }
549
730
 
550
731
  for (const r of data.results) {
551
732
  console.log(`\n${kindIcon(r.kind)} ${r.name} (${r.kind}) -- ${r.file}:${r.line}\n`);
552
733
  if (r.callees.length > 0) {
553
734
  console.log(` -> Calls (${r.callees.length}):`);
554
- for (const c of r.callees) console.log(` -> ${kindIcon(c.kind)} ${c.name} ${c.file}:${c.line}`);
735
+ for (const c of r.callees)
736
+ console.log(` -> ${kindIcon(c.kind)} ${c.name} ${c.file}:${c.line}`);
555
737
  }
556
738
  if (r.callers.length > 0) {
557
739
  console.log(`\n <- Called by (${r.callers.length}):`);
@@ -561,8 +743,13 @@ export function fnDeps(name, customDbPath, opts = {}) {
561
743
  }
562
744
  }
563
745
  for (const [d, fns] of Object.entries(r.transitiveCallers)) {
564
- console.log(`\n ${'<-'.repeat(parseInt(d))} Transitive callers (depth ${d}, ${fns.length}):`);
565
- for (const n of fns.slice(0, 20)) console.log(` ${' '.repeat(parseInt(d)-1)}<- ${kindIcon(n.kind)} ${n.name} ${n.file}:${n.line}`);
746
+ console.log(
747
+ `\n ${'<-'.repeat(parseInt(d, 10))} Transitive callers (depth ${d}, ${fns.length}):`,
748
+ );
749
+ for (const n of fns.slice(0, 20))
750
+ console.log(
751
+ ` ${' '.repeat(parseInt(d, 10) - 1)}<- ${kindIcon(n.kind)} ${n.name} ${n.file}:${n.line}`,
752
+ );
566
753
  if (fns.length > 20) console.log(` ... and ${fns.length - 20} more`);
567
754
  }
568
755
  if (r.callees.length === 0 && r.callers.length === 0) {
@@ -574,8 +761,14 @@ export function fnDeps(name, customDbPath, opts = {}) {
574
761
 
575
762
  export function fnImpact(name, customDbPath, opts = {}) {
576
763
  const data = fnImpactData(name, customDbPath, opts);
577
- if (opts.json) { console.log(JSON.stringify(data, null, 2)); return; }
578
- if (data.results.length === 0) { console.log(`No function/method/class matching "${name}"`); return; }
764
+ if (opts.json) {
765
+ console.log(JSON.stringify(data, null, 2));
766
+ return;
767
+ }
768
+ if (data.results.length === 0) {
769
+ console.log(`No function/method/class matching "${name}"`);
770
+ return;
771
+ }
579
772
 
580
773
  for (const r of data.results) {
581
774
  console.log(`\nFunction impact: ${kindIcon(r.kind)} ${r.name} -- ${r.file}:${r.line}\n`);
@@ -583,9 +776,10 @@ export function fnImpact(name, customDbPath, opts = {}) {
583
776
  console.log(` No callers found.`);
584
777
  } else {
585
778
  for (const [level, fns] of Object.entries(r.levels).sort((a, b) => a[0] - b[0])) {
586
- const l = parseInt(level);
779
+ const l = parseInt(level, 10);
587
780
  console.log(` ${'--'.repeat(l)} Level ${level} (${fns.length} functions):`);
588
- for (const f of fns.slice(0, 20)) console.log(` ${' '.repeat(l)}^ ${kindIcon(f.kind)} ${f.name} ${f.file}:${f.line}`);
781
+ for (const f of fns.slice(0, 20))
782
+ console.log(` ${' '.repeat(l)}^ ${kindIcon(f.kind)} ${f.name} ${f.file}:${f.line}`);
589
783
  if (fns.length > 20) console.log(` ... and ${fns.length - 20} more`);
590
784
  }
591
785
  }
@@ -595,11 +789,22 @@ export function fnImpact(name, customDbPath, opts = {}) {
595
789
 
596
790
  export function diffImpact(customDbPath, opts = {}) {
597
791
  const data = diffImpactData(customDbPath, opts);
598
- if (opts.json) { console.log(JSON.stringify(data, null, 2)); return; }
599
- if (data.error) { console.log(data.error); return; }
600
- if (data.changedFiles === 0) { console.log('No changes detected.'); return; }
792
+ if (opts.json) {
793
+ console.log(JSON.stringify(data, null, 2));
794
+ return;
795
+ }
796
+ if (data.error) {
797
+ console.log(data.error);
798
+ return;
799
+ }
800
+ if (data.changedFiles === 0) {
801
+ console.log('No changes detected.');
802
+ return;
803
+ }
601
804
  if (data.affectedFunctions.length === 0) {
602
- console.log(' No function-level changes detected (changes may be in imports, types, or config).');
805
+ console.log(
806
+ ' No function-level changes detected (changes may be in imports, types, or config).',
807
+ );
603
808
  return;
604
809
  }
605
810
 
@@ -610,7 +815,8 @@ export function diffImpact(customDbPath, opts = {}) {
610
815
  if (fn.transitiveCallers > 0) console.log(` ^ ${fn.transitiveCallers} transitive callers`);
611
816
  }
612
817
  if (data.summary) {
613
- console.log(`\n Summary: ${data.summary.functionsChanged} functions changed -> ${data.summary.callersAffected} callers affected across ${data.summary.filesAffected} files\n`);
818
+ console.log(
819
+ `\n Summary: ${data.summary.functionsChanged} functions changed -> ${data.summary.callersAffected} callers affected across ${data.summary.filesAffected} files\n`,
820
+ );
614
821
  }
615
822
  }
616
-