@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
package/src/queries.js CHANGED
@@ -6,22 +6,39 @@ import { coChangeForFiles } from './cochange.js';
6
6
  import { loadConfig } from './config.js';
7
7
  import { findCycles } from './cycles.js';
8
8
  import {
9
+ countCrossFileCallers,
10
+ findAllIncomingEdges,
11
+ findAllOutgoingEdges,
12
+ findCallees,
13
+ findCallers,
14
+ findCrossFileCallTargets,
9
15
  findDbPath,
16
+ findDistinctCallers,
17
+ findFileNodes,
18
+ findImportDependents,
19
+ findImportSources,
20
+ findImportTargets,
21
+ findIntraFileCallEdges,
22
+ findNodeById,
23
+ findNodeChildren,
24
+ findNodesByFile,
10
25
  findNodesWithFanIn,
26
+ getClassHierarchy,
27
+ getComplexityForNode,
11
28
  iterateFunctionNodes,
12
29
  listFunctionNodes,
13
30
  openReadonlyOrFail,
14
31
  testFilterSQL,
15
32
  } from './db.js';
33
+ import { isTestFile } from './infrastructure/test-filter.js';
16
34
  import { ALL_SYMBOL_KINDS } from './kinds.js';
17
35
  import { debug } from './logger.js';
18
36
  import { ownersForFiles } from './owners.js';
19
37
  import { paginateResult } from './paginate.js';
20
38
  import { LANGUAGE_REGISTRY } from './parser.js';
21
- import { isTestFile } from './test-filter.js';
22
39
 
23
40
  // Re-export from dedicated module for backward compat
24
- export { isTestFile, TEST_PATTERN } from './test-filter.js';
41
+ export { isTestFile, TEST_PATTERN } from './infrastructure/test-filter.js';
25
42
 
26
43
  /**
27
44
  * Resolve a file path relative to repoRoot, rejecting traversal outside the repo.
@@ -79,30 +96,6 @@ export {
79
96
  VALID_ROLES,
80
97
  } from './kinds.js';
81
98
 
82
- /**
83
- * Get all ancestor class names for a given class using extends edges.
84
- */
85
- function getClassHierarchy(db, classNodeId) {
86
- const ancestors = new Set();
87
- const queue = [classNodeId];
88
- while (queue.length > 0) {
89
- const current = queue.shift();
90
- const parents = db
91
- .prepare(`
92
- SELECT n.id, n.name FROM edges e JOIN nodes n ON e.target_id = n.id
93
- WHERE e.source_id = ? AND e.kind = 'extends'
94
- `)
95
- .all(current);
96
- for (const p of parents) {
97
- if (!ancestors.has(p.id)) {
98
- ancestors.add(p.id);
99
- queue.push(p.id);
100
- }
101
- }
102
- }
103
- return ancestors;
104
- }
105
-
106
99
  function resolveMethodViaHierarchy(db, methodName) {
107
100
  const methods = db
108
101
  .prepare(`SELECT * FROM nodes WHERE kind = 'method' AND name LIKE ?`)
@@ -203,21 +196,9 @@ export function queryNameData(name, customDbPath, opts = {}) {
203
196
 
204
197
  const hc = new Map();
205
198
  const results = nodes.map((node) => {
206
- let callees = db
207
- .prepare(`
208
- SELECT n.name, n.kind, n.file, n.line, e.kind as edge_kind
209
- FROM edges e JOIN nodes n ON e.target_id = n.id
210
- WHERE e.source_id = ?
211
- `)
212
- .all(node.id);
199
+ let callees = findAllOutgoingEdges(db, node.id);
213
200
 
214
- let callers = db
215
- .prepare(`
216
- SELECT n.name, n.kind, n.file, n.line, e.kind as edge_kind
217
- FROM edges e JOIN nodes n ON e.source_id = n.id
218
- WHERE e.target_id = ?
219
- `)
220
- .all(node.id);
201
+ let callers = findAllIncomingEdges(db, node.id);
221
202
 
222
203
  if (noTests) {
223
204
  callees = callees.filter((c) => !isTestFile(c.file));
@@ -254,9 +235,7 @@ export function impactAnalysisData(file, customDbPath, opts = {}) {
254
235
  const db = openReadonlyOrFail(customDbPath);
255
236
  try {
256
237
  const noTests = opts.noTests || false;
257
- const fileNodes = db
258
- .prepare(`SELECT * FROM nodes WHERE file LIKE ? AND kind = 'file'`)
259
- .all(`%${file}%`);
238
+ const fileNodes = findFileNodes(db, `%${file}%`);
260
239
  if (fileNodes.length === 0) {
261
240
  return { file, sources: [], levels: {}, totalDependents: 0 };
262
241
  }
@@ -274,12 +253,7 @@ export function impactAnalysisData(file, customDbPath, opts = {}) {
274
253
  while (queue.length > 0) {
275
254
  const current = queue.shift();
276
255
  const level = levels.get(current);
277
- const dependents = db
278
- .prepare(`
279
- SELECT n.* FROM edges e JOIN nodes n ON e.source_id = n.id
280
- WHERE e.target_id = ? AND e.kind IN ('imports', 'imports-type')
281
- `)
282
- .all(current);
256
+ const dependents = findImportDependents(db, current);
283
257
  for (const dep of dependents) {
284
258
  if (!visited.has(dep.id) && (!noTests || !isTestFile(dep.file))) {
285
259
  visited.add(dep.id);
@@ -293,7 +267,7 @@ export function impactAnalysisData(file, customDbPath, opts = {}) {
293
267
  for (const [id, level] of levels) {
294
268
  if (level === 0) continue;
295
269
  if (!byLevel[level]) byLevel[level] = [];
296
- const node = db.prepare('SELECT * FROM nodes WHERE id = ?').get(id);
270
+ const node = findNodeById(db, id);
297
271
  if (node) byLevel[level].push({ file: node.file });
298
272
  }
299
273
 
@@ -350,33 +324,19 @@ export function fileDepsData(file, customDbPath, opts = {}) {
350
324
  const db = openReadonlyOrFail(customDbPath);
351
325
  try {
352
326
  const noTests = opts.noTests || false;
353
- const fileNodes = db
354
- .prepare(`SELECT * FROM nodes WHERE file LIKE ? AND kind = 'file'`)
355
- .all(`%${file}%`);
327
+ const fileNodes = findFileNodes(db, `%${file}%`);
356
328
  if (fileNodes.length === 0) {
357
329
  return { file, results: [] };
358
330
  }
359
331
 
360
332
  const results = fileNodes.map((fn) => {
361
- let importsTo = db
362
- .prepare(`
363
- SELECT n.file, e.kind as edge_kind FROM edges e JOIN nodes n ON e.target_id = n.id
364
- WHERE e.source_id = ? AND e.kind IN ('imports', 'imports-type')
365
- `)
366
- .all(fn.id);
333
+ let importsTo = findImportTargets(db, fn.id);
367
334
  if (noTests) importsTo = importsTo.filter((i) => !isTestFile(i.file));
368
335
 
369
- let importedBy = db
370
- .prepare(`
371
- SELECT n.file, e.kind as edge_kind FROM edges e JOIN nodes n ON e.source_id = n.id
372
- WHERE e.target_id = ? AND e.kind IN ('imports', 'imports-type')
373
- `)
374
- .all(fn.id);
336
+ let importedBy = findImportSources(db, fn.id);
375
337
  if (noTests) importedBy = importedBy.filter((i) => !isTestFile(i.file));
376
338
 
377
- const defs = db
378
- .prepare(`SELECT * FROM nodes WHERE file = ? AND kind != 'file' ORDER BY line`)
379
- .all(fn.file);
339
+ const defs = findNodesByFile(db, fn.file);
380
340
 
381
341
  return {
382
342
  file: fn.file,
@@ -406,35 +366,17 @@ export function fnDepsData(name, customDbPath, opts = {}) {
406
366
  }
407
367
 
408
368
  const results = nodes.map((node) => {
409
- const callees = db
410
- .prepare(`
411
- SELECT n.name, n.kind, n.file, n.line, e.kind as edge_kind
412
- FROM edges e JOIN nodes n ON e.target_id = n.id
413
- WHERE e.source_id = ? AND e.kind = 'calls'
414
- `)
415
- .all(node.id);
369
+ const callees = findCallees(db, node.id);
416
370
  const filteredCallees = noTests ? callees.filter((c) => !isTestFile(c.file)) : callees;
417
371
 
418
- let callers = db
419
- .prepare(`
420
- SELECT n.name, n.kind, n.file, n.line, e.kind as edge_kind
421
- FROM edges e JOIN nodes n ON e.source_id = n.id
422
- WHERE e.target_id = ? AND e.kind = 'calls'
423
- `)
424
- .all(node.id);
372
+ let callers = findCallers(db, node.id);
425
373
 
426
374
  if (node.kind === 'method' && node.name.includes('.')) {
427
375
  const methodName = node.name.split('.').pop();
428
376
  const relatedMethods = resolveMethodViaHierarchy(db, methodName);
429
377
  for (const rm of relatedMethods) {
430
378
  if (rm.id === node.id) continue;
431
- const extraCallers = db
432
- .prepare(`
433
- SELECT n.name, n.kind, n.file, n.line, e.kind as edge_kind
434
- FROM edges e JOIN nodes n ON e.source_id = n.id
435
- WHERE e.target_id = ? AND e.kind = 'calls'
436
- `)
437
- .all(rm.id);
379
+ const extraCallers = findCallers(db, rm.id);
438
380
  callers.push(...extraCallers.map((c) => ({ ...c, viaHierarchy: rm.name })));
439
381
  }
440
382
  }
@@ -536,13 +478,7 @@ export function fnImpactData(name, customDbPath, opts = {}) {
536
478
  for (let d = 1; d <= maxDepth; d++) {
537
479
  const nextFrontier = [];
538
480
  for (const fid of frontier) {
539
- const callers = db
540
- .prepare(`
541
- SELECT DISTINCT n.id, n.name, n.kind, n.file, n.line
542
- FROM edges e JOIN nodes n ON e.source_id = n.id
543
- WHERE e.target_id = ? AND e.kind = 'calls'
544
- `)
545
- .all(fid);
481
+ const callers = findDistinctCallers(db, fid);
546
482
  for (const c of callers) {
547
483
  if (!visited.has(c.id) && (!noTests || !isTestFile(c.file))) {
548
484
  visited.add(c.id);
@@ -884,13 +820,7 @@ export function diffImpactData(customDbPath, opts = {}) {
884
820
  for (let d = 1; d <= maxDepth; d++) {
885
821
  const nextFrontier = [];
886
822
  for (const fid of frontier) {
887
- const callers = db
888
- .prepare(`
889
- SELECT DISTINCT n.id, n.name, n.kind, n.file, n.line
890
- FROM edges e JOIN nodes n ON e.source_id = n.id
891
- WHERE e.target_id = ? AND e.kind = 'calls'
892
- `)
893
- .all(fid);
823
+ const callers = findDistinctCallers(db, fid);
894
824
  for (const c of callers) {
895
825
  if (!visited.has(c.id) && (!noTests || !isTestFile(c.file))) {
896
826
  visited.add(c.id);
@@ -1658,13 +1588,7 @@ export function contextData(name, customDbPath, opts = {}) {
1658
1588
  const signature = fileLines ? extractSignature(fileLines, node.line) : null;
1659
1589
 
1660
1590
  // Callees
1661
- const calleeRows = db
1662
- .prepare(
1663
- `SELECT n.id, n.name, n.kind, n.file, n.line, n.end_line
1664
- FROM edges e JOIN nodes n ON e.target_id = n.id
1665
- WHERE e.source_id = ? AND e.kind = 'calls'`,
1666
- )
1667
- .all(node.id);
1591
+ const calleeRows = findCallees(db, node.id);
1668
1592
  const filteredCallees = noTests ? calleeRows.filter((c) => !isTestFile(c.file)) : calleeRows;
1669
1593
 
1670
1594
  const callees = filteredCallees.map((c) => {
@@ -1694,13 +1618,7 @@ export function contextData(name, customDbPath, opts = {}) {
1694
1618
  for (let d = 2; d <= maxDepth; d++) {
1695
1619
  const nextFrontier = [];
1696
1620
  for (const fid of frontier) {
1697
- const deeper = db
1698
- .prepare(
1699
- `SELECT n.id, n.name, n.kind, n.file, n.line, n.end_line
1700
- FROM edges e JOIN nodes n ON e.target_id = n.id
1701
- WHERE e.source_id = ? AND e.kind = 'calls'`,
1702
- )
1703
- .all(fid);
1621
+ const deeper = findCallees(db, fid);
1704
1622
  for (const c of deeper) {
1705
1623
  if (!visited.has(c.id) && (!noTests || !isTestFile(c.file))) {
1706
1624
  visited.add(c.id);
@@ -1724,13 +1642,7 @@ export function contextData(name, customDbPath, opts = {}) {
1724
1642
  }
1725
1643
 
1726
1644
  // Callers
1727
- let callerRows = db
1728
- .prepare(
1729
- `SELECT n.name, n.kind, n.file, n.line
1730
- FROM edges e JOIN nodes n ON e.source_id = n.id
1731
- WHERE e.target_id = ? AND e.kind = 'calls'`,
1732
- )
1733
- .all(node.id);
1645
+ let callerRows = findCallers(db, node.id);
1734
1646
 
1735
1647
  // Method hierarchy resolution
1736
1648
  if (node.kind === 'method' && node.name.includes('.')) {
@@ -1738,13 +1650,7 @@ export function contextData(name, customDbPath, opts = {}) {
1738
1650
  const relatedMethods = resolveMethodViaHierarchy(db, methodName);
1739
1651
  for (const rm of relatedMethods) {
1740
1652
  if (rm.id === node.id) continue;
1741
- const extraCallers = db
1742
- .prepare(
1743
- `SELECT n.name, n.kind, n.file, n.line
1744
- FROM edges e JOIN nodes n ON e.source_id = n.id
1745
- WHERE e.target_id = ? AND e.kind = 'calls'`,
1746
- )
1747
- .all(rm.id);
1653
+ const extraCallers = findCallers(db, rm.id);
1748
1654
  callerRows.push(...extraCallers.map((c) => ({ ...c, viaHierarchy: rm.name })));
1749
1655
  }
1750
1656
  }
@@ -1759,13 +1665,7 @@ export function contextData(name, customDbPath, opts = {}) {
1759
1665
  }));
1760
1666
 
1761
1667
  // Related tests: callers that live in test files
1762
- const testCallerRows = db
1763
- .prepare(
1764
- `SELECT n.name, n.kind, n.file, n.line
1765
- FROM edges e JOIN nodes n ON e.source_id = n.id
1766
- WHERE e.target_id = ? AND e.kind = 'calls'`,
1767
- )
1768
- .all(node.id);
1668
+ const testCallerRows = findCallers(db, node.id);
1769
1669
  const testCallers = testCallerRows.filter((c) => isTestFile(c.file));
1770
1670
 
1771
1671
  const testsByFile = new Map();
@@ -1796,11 +1696,7 @@ export function contextData(name, customDbPath, opts = {}) {
1796
1696
  // Complexity metrics
1797
1697
  let complexityMetrics = null;
1798
1698
  try {
1799
- const cRow = db
1800
- .prepare(
1801
- 'SELECT cognitive, cyclomatic, max_nesting, maintainability_index, halstead_volume FROM function_complexity WHERE node_id = ?',
1802
- )
1803
- .get(node.id);
1699
+ const cRow = getComplexityForNode(db, node.id);
1804
1700
  if (cRow) {
1805
1701
  complexityMetrics = {
1806
1702
  cognitive: cRow.cognitive,
@@ -1817,10 +1713,12 @@ export function contextData(name, customDbPath, opts = {}) {
1817
1713
  // Children (parameters, properties, constants)
1818
1714
  let nodeChildren = [];
1819
1715
  try {
1820
- nodeChildren = db
1821
- .prepare('SELECT name, kind, line, end_line FROM nodes WHERE parent_id = ? ORDER BY line')
1822
- .all(node.id)
1823
- .map((c) => ({ name: c.name, kind: c.kind, line: c.line, endLine: c.end_line || null }));
1716
+ nodeChildren = findNodeChildren(db, node.id).map((c) => ({
1717
+ name: c.name,
1718
+ kind: c.kind,
1719
+ line: c.line,
1720
+ endLine: c.end_line || null,
1721
+ }));
1824
1722
  } catch {
1825
1723
  /* parent_id column may not exist */
1826
1724
  }
@@ -1864,9 +1762,7 @@ export function childrenData(name, customDbPath, opts = {}) {
1864
1762
  const results = nodes.map((node) => {
1865
1763
  let children;
1866
1764
  try {
1867
- children = db
1868
- .prepare('SELECT name, kind, line, end_line FROM nodes WHERE parent_id = ? ORDER BY line')
1869
- .all(node.id);
1765
+ children = findNodeChildren(db, node.id);
1870
1766
  } catch {
1871
1767
  children = [];
1872
1768
  }
@@ -1905,28 +1801,14 @@ function isFileLikeTarget(target) {
1905
1801
  }
1906
1802
 
1907
1803
  function explainFileImpl(db, target, getFileLines) {
1908
- const fileNodes = db
1909
- .prepare(`SELECT * FROM nodes WHERE file LIKE ? AND kind = 'file'`)
1910
- .all(`%${target}%`);
1804
+ const fileNodes = findFileNodes(db, `%${target}%`);
1911
1805
  if (fileNodes.length === 0) return [];
1912
1806
 
1913
1807
  return fileNodes.map((fn) => {
1914
- const symbols = db
1915
- .prepare(`SELECT * FROM nodes WHERE file = ? AND kind != 'file' ORDER BY line`)
1916
- .all(fn.file);
1808
+ const symbols = findNodesByFile(db, fn.file);
1917
1809
 
1918
1810
  // IDs of symbols that have incoming calls from other files (public)
1919
- const publicIds = new Set(
1920
- db
1921
- .prepare(
1922
- `SELECT DISTINCT e.target_id FROM edges e
1923
- JOIN nodes caller ON e.source_id = caller.id
1924
- JOIN nodes target ON e.target_id = target.id
1925
- WHERE target.file = ? AND caller.file != ? AND e.kind = 'calls'`,
1926
- )
1927
- .all(fn.file, fn.file)
1928
- .map((r) => r.target_id),
1929
- );
1811
+ const publicIds = findCrossFileCallTargets(db, fn.file);
1930
1812
 
1931
1813
  const fileLines = getFileLines(fn.file);
1932
1814
  const mapSymbol = (s) => ({
@@ -1942,33 +1824,12 @@ function explainFileImpl(db, target, getFileLines) {
1942
1824
  const internal = symbols.filter((s) => !publicIds.has(s.id)).map(mapSymbol);
1943
1825
 
1944
1826
  // Imports / importedBy
1945
- const imports = db
1946
- .prepare(
1947
- `SELECT n.file FROM edges e JOIN nodes n ON e.target_id = n.id
1948
- WHERE e.source_id = ? AND e.kind IN ('imports', 'imports-type')`,
1949
- )
1950
- .all(fn.id)
1951
- .map((r) => ({ file: r.file }));
1827
+ const imports = findImportTargets(db, fn.id).map((r) => ({ file: r.file }));
1952
1828
 
1953
- const importedBy = db
1954
- .prepare(
1955
- `SELECT n.file FROM edges e JOIN nodes n ON e.source_id = n.id
1956
- WHERE e.target_id = ? AND e.kind IN ('imports', 'imports-type')`,
1957
- )
1958
- .all(fn.id)
1959
- .map((r) => ({ file: r.file }));
1829
+ const importedBy = findImportSources(db, fn.id).map((r) => ({ file: r.file }));
1960
1830
 
1961
1831
  // Intra-file data flow
1962
- const intraEdges = db
1963
- .prepare(
1964
- `SELECT caller.name as caller_name, callee.name as callee_name
1965
- FROM edges e
1966
- JOIN nodes caller ON e.source_id = caller.id
1967
- JOIN nodes callee ON e.target_id = callee.id
1968
- WHERE caller.file = ? AND callee.file = ? AND e.kind = 'calls'
1969
- ORDER BY caller.line`,
1970
- )
1971
- .all(fn.file, fn.file);
1832
+ const intraEdges = findIntraFileCallEdges(db, fn.file);
1972
1833
 
1973
1834
  const dataFlowMap = new Map();
1974
1835
  for (const edge of intraEdges) {
@@ -2021,43 +1882,31 @@ function explainFunctionImpl(db, target, noTests, getFileLines) {
2021
1882
  const summary = fileLines ? extractSummary(fileLines, node.line) : null;
2022
1883
  const signature = fileLines ? extractSignature(fileLines, node.line) : null;
2023
1884
 
2024
- const callees = db
2025
- .prepare(
2026
- `SELECT n.name, n.kind, n.file, n.line
2027
- FROM edges e JOIN nodes n ON e.target_id = n.id
2028
- WHERE e.source_id = ? AND e.kind = 'calls'`,
2029
- )
2030
- .all(node.id)
2031
- .map((c) => ({ name: c.name, kind: c.kind, file: c.file, line: c.line }));
1885
+ const callees = findCallees(db, node.id).map((c) => ({
1886
+ name: c.name,
1887
+ kind: c.kind,
1888
+ file: c.file,
1889
+ line: c.line,
1890
+ }));
2032
1891
 
2033
- let callers = db
2034
- .prepare(
2035
- `SELECT n.name, n.kind, n.file, n.line
2036
- FROM edges e JOIN nodes n ON e.source_id = n.id
2037
- WHERE e.target_id = ? AND e.kind = 'calls'`,
2038
- )
2039
- .all(node.id)
2040
- .map((c) => ({ name: c.name, kind: c.kind, file: c.file, line: c.line }));
1892
+ let callers = findCallers(db, node.id).map((c) => ({
1893
+ name: c.name,
1894
+ kind: c.kind,
1895
+ file: c.file,
1896
+ line: c.line,
1897
+ }));
2041
1898
  if (noTests) callers = callers.filter((c) => !isTestFile(c.file));
2042
1899
 
2043
- const testCallerRows = db
2044
- .prepare(
2045
- `SELECT DISTINCT n.file FROM edges e JOIN nodes n ON e.source_id = n.id
2046
- WHERE e.target_id = ? AND e.kind = 'calls'`,
2047
- )
2048
- .all(node.id);
1900
+ const testCallerRows = findCallers(db, node.id);
1901
+ const seenFiles = new Set();
2049
1902
  const relatedTests = testCallerRows
2050
- .filter((r) => isTestFile(r.file))
1903
+ .filter((r) => isTestFile(r.file) && !seenFiles.has(r.file) && seenFiles.add(r.file))
2051
1904
  .map((r) => ({ file: r.file }));
2052
1905
 
2053
1906
  // Complexity metrics
2054
1907
  let complexityMetrics = null;
2055
1908
  try {
2056
- const cRow = db
2057
- .prepare(
2058
- 'SELECT cognitive, cyclomatic, max_nesting, maintainability_index, halstead_volume FROM function_complexity WHERE node_id = ?',
2059
- )
2060
- .get(node.id);
1909
+ const cRow = getComplexityForNode(db, node.id);
2061
1910
  if (cRow) {
2062
1911
  complexityMetrics = {
2063
1912
  cognitive: cRow.cognitive,
@@ -2204,20 +2053,10 @@ function whereSymbolImpl(db, target, noTests) {
2204
2053
 
2205
2054
  const hc = new Map();
2206
2055
  return nodes.map((node) => {
2207
- const crossFileCallers = db
2208
- .prepare(
2209
- `SELECT COUNT(*) as cnt FROM edges e JOIN nodes n ON e.source_id = n.id
2210
- WHERE e.target_id = ? AND e.kind = 'calls' AND n.file != ?`,
2211
- )
2212
- .get(node.id, node.file);
2213
- const exported = crossFileCallers.cnt > 0;
2056
+ const crossCount = countCrossFileCallers(db, node.id, node.file);
2057
+ const exported = crossCount > 0;
2214
2058
 
2215
- let uses = db
2216
- .prepare(
2217
- `SELECT n.name, n.file, n.line FROM edges e JOIN nodes n ON e.source_id = n.id
2218
- WHERE e.target_id = ? AND e.kind = 'calls'`,
2219
- )
2220
- .all(node.id);
2059
+ let uses = findCallers(db, node.id);
2221
2060
  if (noTests) uses = uses.filter((u) => !isTestFile(u.file));
2222
2061
 
2223
2062
  return {
@@ -2229,43 +2068,17 @@ function whereSymbolImpl(db, target, noTests) {
2229
2068
  }
2230
2069
 
2231
2070
  function whereFileImpl(db, target) {
2232
- const fileNodes = db
2233
- .prepare(`SELECT * FROM nodes WHERE file LIKE ? AND kind = 'file'`)
2234
- .all(`%${target}%`);
2071
+ const fileNodes = findFileNodes(db, `%${target}%`);
2235
2072
  if (fileNodes.length === 0) return [];
2236
2073
 
2237
2074
  return fileNodes.map((fn) => {
2238
- const symbols = db
2239
- .prepare(`SELECT * FROM nodes WHERE file = ? AND kind != 'file' ORDER BY line`)
2240
- .all(fn.file);
2075
+ const symbols = findNodesByFile(db, fn.file);
2241
2076
 
2242
- const imports = db
2243
- .prepare(
2244
- `SELECT n.file FROM edges e JOIN nodes n ON e.target_id = n.id
2245
- WHERE e.source_id = ? AND e.kind IN ('imports', 'imports-type')`,
2246
- )
2247
- .all(fn.id)
2248
- .map((r) => r.file);
2077
+ const imports = findImportTargets(db, fn.id).map((r) => r.file);
2249
2078
 
2250
- const importedBy = db
2251
- .prepare(
2252
- `SELECT n.file FROM edges e JOIN nodes n ON e.source_id = n.id
2253
- WHERE e.target_id = ? AND e.kind IN ('imports', 'imports-type')`,
2254
- )
2255
- .all(fn.id)
2256
- .map((r) => r.file);
2079
+ const importedBy = findImportSources(db, fn.id).map((r) => r.file);
2257
2080
 
2258
- const exportedIds = new Set(
2259
- db
2260
- .prepare(
2261
- `SELECT DISTINCT e.target_id FROM edges e
2262
- JOIN nodes caller ON e.source_id = caller.id
2263
- JOIN nodes target ON e.target_id = target.id
2264
- WHERE target.file = ? AND caller.file != ? AND e.kind = 'calls'`,
2265
- )
2266
- .all(fn.file, fn.file)
2267
- .map((r) => r.target_id),
2268
- );
2081
+ const exportedIds = findCrossFileCallTargets(db, fn.file);
2269
2082
 
2270
2083
  const exported = symbols.filter((s) => exportedIds.has(s.id)).map((s) => s.name);
2271
2084
 
@@ -2341,9 +2154,7 @@ export function rolesData(customDbPath, opts = {}) {
2341
2154
  // ─── exportsData ─────────────────────────────────────────────────────
2342
2155
 
2343
2156
  function exportsFileImpl(db, target, noTests, getFileLines, unused) {
2344
- const fileNodes = db
2345
- .prepare(`SELECT * FROM nodes WHERE file LIKE ? AND kind = 'file'`)
2346
- .all(`%${target}%`);
2157
+ const fileNodes = findFileNodes(db, `%${target}%`);
2347
2158
  if (fileNodes.length === 0) return [];
2348
2159
 
2349
2160
  // Detect whether exported column exists
@@ -2356,9 +2167,7 @@ function exportsFileImpl(db, target, noTests, getFileLines, unused) {
2356
2167
  }
2357
2168
 
2358
2169
  return fileNodes.map((fn) => {
2359
- const symbols = db
2360
- .prepare(`SELECT * FROM nodes WHERE file = ? AND kind != 'file' ORDER BY line`)
2361
- .all(fn.file);
2170
+ const symbols = findNodesByFile(db, fn.file);
2362
2171
 
2363
2172
  let exported;
2364
2173
  if (hasExportedCol) {
@@ -2370,17 +2179,7 @@ function exportsFileImpl(db, target, noTests, getFileLines, unused) {
2370
2179
  .all(fn.file);
2371
2180
  } else {
2372
2181
  // Fallback: symbols that have incoming calls from other files
2373
- const exportedIds = new Set(
2374
- db
2375
- .prepare(
2376
- `SELECT DISTINCT e.target_id FROM edges e
2377
- JOIN nodes caller ON e.source_id = caller.id
2378
- JOIN nodes target ON e.target_id = target.id
2379
- WHERE target.file = ? AND caller.file != ? AND e.kind = 'calls'`,
2380
- )
2381
- .all(fn.file, fn.file)
2382
- .map((r) => r.target_id),
2383
- );
2182
+ const exportedIds = findCrossFileCallTargets(db, fn.file);
2384
2183
  exported = symbols.filter((s) => exportedIds.has(s.id));
2385
2184
  }
2386
2185
  const internalCount = symbols.length - exported.length;
package/src/sequence.js CHANGED
@@ -6,12 +6,11 @@
6
6
  * sequence-diagram conventions.
7
7
  */
8
8
 
9
- import { openReadonlyOrFail } from './db.js';
9
+ import { findCallees, openReadonlyOrFail } from './db.js';
10
+ import { isTestFile } from './infrastructure/test-filter.js';
10
11
  import { paginateResult } from './paginate.js';
11
- import { findMatchingNodes, kindIcon } from './queries.js';
12
- import { outputResult } from './result-formatter.js';
12
+ import { findMatchingNodes } from './queries.js';
13
13
  import { FRAMEWORK_ENTRY_PREFIXES } from './structure.js';
14
- import { isTestFile } from './test-filter.js';
15
14
 
16
15
  // ─── Alias generation ────────────────────────────────────────────────
17
16
 
@@ -130,17 +129,11 @@ export function sequenceData(name, dbPath, opts = {}) {
130
129
  idToNode.set(matchNode.id, matchNode);
131
130
  let truncated = false;
132
131
 
133
- const getCallees = db.prepare(
134
- `SELECT DISTINCT n.id, n.name, n.kind, n.file, n.line
135
- FROM edges e JOIN nodes n ON e.target_id = n.id
136
- WHERE e.source_id = ? AND e.kind = 'calls'`,
137
- );
138
-
139
132
  for (let d = 1; d <= maxDepth; d++) {
140
133
  const nextFrontier = [];
141
134
 
142
135
  for (const fid of frontier) {
143
- const callees = getCallees.all(fid);
136
+ const callees = findCallees(db, fid);
144
137
 
145
138
  const caller = idToNode.get(fid);
146
139
 
@@ -170,7 +163,7 @@ export function sequenceData(name, dbPath, opts = {}) {
170
163
 
171
164
  if (d === maxDepth && frontier.length > 0) {
172
165
  // Only mark truncated if at least one frontier node has further callees
173
- const hasMoreCalls = frontier.some((fid) => getCallees.all(fid).length > 0);
166
+ const hasMoreCalls = frontier.some((fid) => findCallees(db, fid).length > 0);
174
167
  if (hasMoreCalls) truncated = true;
175
168
  }
176
169
  }
@@ -330,35 +323,3 @@ export function sequenceToMermaid(seqResult) {
330
323
 
331
324
  return lines.join('\n');
332
325
  }
333
-
334
- // ─── CLI formatter ───────────────────────────────────────────────────
335
-
336
- /**
337
- * CLI entry point — format sequence data as mermaid, JSON, or ndjson.
338
- */
339
- export function sequence(name, dbPath, opts = {}) {
340
- const data = sequenceData(name, dbPath, opts);
341
-
342
- if (outputResult(data, 'messages', opts)) return;
343
-
344
- // Default: mermaid format
345
- if (!data.entry) {
346
- console.log(`No matching function found for "${name}".`);
347
- return;
348
- }
349
-
350
- const e = data.entry;
351
- console.log(`\nSequence from: [${kindIcon(e.kind)}] ${e.name} ${e.file}:${e.line}`);
352
- console.log(`Participants: ${data.participants.length} Messages: ${data.totalMessages}`);
353
- if (data.truncated) {
354
- console.log(` (truncated at depth ${data.depth})`);
355
- }
356
- console.log();
357
-
358
- if (data.messages.length === 0) {
359
- console.log(' (leaf node — no callees)');
360
- return;
361
- }
362
-
363
- console.log(sequenceToMermaid(data));
364
- }