@optave/codegraph 1.1.0 → 1.3.0

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();
@@ -456,20 +570,28 @@ export function diffImpactData(customDbPath, opts = {}) {
456
570
 
457
571
  export function queryName(name, customDbPath, opts = {}) {
458
572
  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; }
573
+ if (opts.json) {
574
+ console.log(JSON.stringify(data, null, 2));
575
+ return;
576
+ }
577
+ if (data.results.length === 0) {
578
+ console.log(`No results for "${name}"`);
579
+ return;
580
+ }
461
581
 
462
582
  console.log(`\nResults for "${name}":\n`);
463
583
  for (const r of data.results) {
464
584
  console.log(` ${kindIcon(r.kind)} ${r.name} (${r.kind}) -- ${r.file}:${r.line}`);
465
585
  if (r.callees.length > 0) {
466
586
  console.log(` -> calls/uses:`);
467
- for (const c of r.callees.slice(0, 15)) console.log(` -> ${c.name} (${c.edgeKind}) ${c.file}:${c.line}`);
587
+ for (const c of r.callees.slice(0, 15))
588
+ console.log(` -> ${c.name} (${c.edgeKind}) ${c.file}:${c.line}`);
468
589
  if (r.callees.length > 15) console.log(` ... and ${r.callees.length - 15} more`);
469
590
  }
470
591
  if (r.callers.length > 0) {
471
592
  console.log(` <- called by:`);
472
- for (const c of r.callers.slice(0, 15)) console.log(` <- ${c.name} (${c.edgeKind}) ${c.file}:${c.line}`);
593
+ for (const c of r.callers.slice(0, 15))
594
+ console.log(` <- ${c.name} (${c.edgeKind}) ${c.file}:${c.line}`);
473
595
  if (r.callers.length > 15) console.log(` ... and ${r.callers.length - 15} more`);
474
596
  }
475
597
  console.log();
@@ -478,8 +600,14 @@ export function queryName(name, customDbPath, opts = {}) {
478
600
 
479
601
  export function impactAnalysis(file, customDbPath, opts = {}) {
480
602
  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; }
603
+ if (opts.json) {
604
+ console.log(JSON.stringify(data, null, 2));
605
+ return;
606
+ }
607
+ if (data.sources.length === 0) {
608
+ console.log(`No file matching "${file}" in graph`);
609
+ return;
610
+ }
483
611
 
484
612
  console.log(`\nImpact analysis for files matching "${file}":\n`);
485
613
  for (const s of data.sources) console.log(` # ${s} (source)`);
@@ -490,8 +618,11 @@ export function impactAnalysis(file, customDbPath, opts = {}) {
490
618
  } else {
491
619
  for (const level of Object.keys(levels).sort((a, b) => a - b)) {
492
620
  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}`);
621
+ console.log(
622
+ `\n ${'--'.repeat(parseInt(level, 10))} Level ${level} (${nodes.length} files):`,
623
+ );
624
+ for (const n of nodes.slice(0, 30))
625
+ console.log(` ${' '.repeat(parseInt(level, 10))}^ ${n.file}`);
495
626
  if (nodes.length > 30) console.log(` ... and ${nodes.length - 30} more`);
496
627
  }
497
628
  }
@@ -500,7 +631,10 @@ export function impactAnalysis(file, customDbPath, opts = {}) {
500
631
 
501
632
  export function moduleMap(customDbPath, limit = 20, opts = {}) {
502
633
  const data = moduleMapData(customDbPath, limit);
503
- if (opts.json) { console.log(JSON.stringify(data, null, 2)); return; }
634
+ if (opts.json) {
635
+ console.log(JSON.stringify(data, null, 2));
636
+ return;
637
+ }
504
638
 
505
639
  console.log(`\nModule map (top ${limit} most-connected nodes):\n`);
506
640
  const dirs = new Map();
@@ -513,16 +647,26 @@ export function moduleMap(customDbPath, limit = 20, opts = {}) {
513
647
  for (const f of files) {
514
648
  const total = f.inEdges + f.outEdges;
515
649
  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}`);
650
+ console.log(
651
+ ` ${path.basename(f.file).padEnd(35)} <-${String(f.inEdges).padStart(3)} ->${String(f.outEdges).padStart(3)} ${bar}`,
652
+ );
517
653
  }
518
654
  }
519
- console.log(`\n Total: ${data.stats.totalFiles} files, ${data.stats.totalNodes} symbols, ${data.stats.totalEdges} edges\n`);
655
+ console.log(
656
+ `\n Total: ${data.stats.totalFiles} files, ${data.stats.totalNodes} symbols, ${data.stats.totalEdges} edges\n`,
657
+ );
520
658
  }
521
659
 
522
660
  export function fileDeps(file, customDbPath, opts = {}) {
523
661
  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; }
662
+ if (opts.json) {
663
+ console.log(JSON.stringify(data, null, 2));
664
+ return;
665
+ }
666
+ if (data.results.length === 0) {
667
+ console.log(`No file matching "${file}" in graph`);
668
+ return;
669
+ }
526
670
 
527
671
  for (const r of data.results) {
528
672
  console.log(`\n# ${r.file}\n`);
@@ -535,7 +679,8 @@ export function fileDeps(file, customDbPath, opts = {}) {
535
679
  for (const i of r.importedBy) console.log(` <- ${i.file}`);
536
680
  if (r.definitions.length > 0) {
537
681
  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}`);
682
+ for (const d of r.definitions.slice(0, 30))
683
+ console.log(` ${kindIcon(d.kind)} ${d.name} :${d.line}`);
539
684
  if (r.definitions.length > 30) console.log(` ... and ${r.definitions.length - 30} more`);
540
685
  }
541
686
  console.log();
@@ -544,14 +689,21 @@ export function fileDeps(file, customDbPath, opts = {}) {
544
689
 
545
690
  export function fnDeps(name, customDbPath, opts = {}) {
546
691
  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; }
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 function/method/class matching "${name}"`);
698
+ return;
699
+ }
549
700
 
550
701
  for (const r of data.results) {
551
702
  console.log(`\n${kindIcon(r.kind)} ${r.name} (${r.kind}) -- ${r.file}:${r.line}\n`);
552
703
  if (r.callees.length > 0) {
553
704
  console.log(` -> Calls (${r.callees.length}):`);
554
- for (const c of r.callees) console.log(` -> ${kindIcon(c.kind)} ${c.name} ${c.file}:${c.line}`);
705
+ for (const c of r.callees)
706
+ console.log(` -> ${kindIcon(c.kind)} ${c.name} ${c.file}:${c.line}`);
555
707
  }
556
708
  if (r.callers.length > 0) {
557
709
  console.log(`\n <- Called by (${r.callers.length}):`);
@@ -561,8 +713,13 @@ export function fnDeps(name, customDbPath, opts = {}) {
561
713
  }
562
714
  }
563
715
  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}`);
716
+ console.log(
717
+ `\n ${'<-'.repeat(parseInt(d, 10))} Transitive callers (depth ${d}, ${fns.length}):`,
718
+ );
719
+ for (const n of fns.slice(0, 20))
720
+ console.log(
721
+ ` ${' '.repeat(parseInt(d, 10) - 1)}<- ${kindIcon(n.kind)} ${n.name} ${n.file}:${n.line}`,
722
+ );
566
723
  if (fns.length > 20) console.log(` ... and ${fns.length - 20} more`);
567
724
  }
568
725
  if (r.callees.length === 0 && r.callers.length === 0) {
@@ -574,8 +731,14 @@ export function fnDeps(name, customDbPath, opts = {}) {
574
731
 
575
732
  export function fnImpact(name, customDbPath, opts = {}) {
576
733
  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; }
734
+ if (opts.json) {
735
+ console.log(JSON.stringify(data, null, 2));
736
+ return;
737
+ }
738
+ if (data.results.length === 0) {
739
+ console.log(`No function/method/class matching "${name}"`);
740
+ return;
741
+ }
579
742
 
580
743
  for (const r of data.results) {
581
744
  console.log(`\nFunction impact: ${kindIcon(r.kind)} ${r.name} -- ${r.file}:${r.line}\n`);
@@ -583,9 +746,10 @@ export function fnImpact(name, customDbPath, opts = {}) {
583
746
  console.log(` No callers found.`);
584
747
  } else {
585
748
  for (const [level, fns] of Object.entries(r.levels).sort((a, b) => a[0] - b[0])) {
586
- const l = parseInt(level);
749
+ const l = parseInt(level, 10);
587
750
  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}`);
751
+ for (const f of fns.slice(0, 20))
752
+ console.log(` ${' '.repeat(l)}^ ${kindIcon(f.kind)} ${f.name} ${f.file}:${f.line}`);
589
753
  if (fns.length > 20) console.log(` ... and ${fns.length - 20} more`);
590
754
  }
591
755
  }
@@ -595,11 +759,22 @@ export function fnImpact(name, customDbPath, opts = {}) {
595
759
 
596
760
  export function diffImpact(customDbPath, opts = {}) {
597
761
  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; }
762
+ if (opts.json) {
763
+ console.log(JSON.stringify(data, null, 2));
764
+ return;
765
+ }
766
+ if (data.error) {
767
+ console.log(data.error);
768
+ return;
769
+ }
770
+ if (data.changedFiles === 0) {
771
+ console.log('No changes detected.');
772
+ return;
773
+ }
601
774
  if (data.affectedFunctions.length === 0) {
602
- console.log(' No function-level changes detected (changes may be in imports, types, or config).');
775
+ console.log(
776
+ ' No function-level changes detected (changes may be in imports, types, or config).',
777
+ );
603
778
  return;
604
779
  }
605
780
 
@@ -610,7 +785,8 @@ export function diffImpact(customDbPath, opts = {}) {
610
785
  if (fn.transitiveCallers > 0) console.log(` ^ ${fn.transitiveCallers} transitive callers`);
611
786
  }
612
787
  if (data.summary) {
613
- console.log(`\n Summary: ${data.summary.functionsChanged} functions changed -> ${data.summary.callersAffected} callers affected across ${data.summary.filesAffected} files\n`);
788
+ console.log(
789
+ `\n Summary: ${data.summary.functionsChanged} functions changed -> ${data.summary.callersAffected} callers affected across ${data.summary.filesAffected} files\n`,
790
+ );
614
791
  }
615
792
  }
616
-